diff --git a/.eslintrc.json b/.eslintrc.json index 8a05e85289..8c4dac435f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,16 +1,37 @@ { - "extends": [ - "@bigcommerce/eslint-config" - ], + "extends": ["@bigcommerce/eslint-config"], + "parser": "@typescript-eslint/parser", "root": true, "plugins": ["@nrwl/nx"], "env": { "browser": true }, + "settings": { + "import/resolver": { + "typescript": { + "project": "./tsconfig.*?.json" + } + } + }, + "parserOptions": { + "project": "./tsconfig.*?.json" + }, "overrides": [ { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], "rules": { + "prettier/prettier": [ + "error", + { + "tabWidth": 4 + } + ], + "no-restricted-syntax": [ + "off", + { + "selector": "ForOfStatement" + } + ], "@nrwl/nx/enforce-module-boundaries": [ "error", { diff --git a/package-lock.json b/package-lock.json index cd5a695db0..88f9c8fce2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1092,6 +1092,16 @@ "regenerator-runtime": "^0.13.4" } }, + "@babel/runtime-corejs3": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.19.0.tgz", + "integrity": "sha512-JyXXoCu1N8GLuKc2ii8y5RGma5FMpFeO2nAQIe0Yzrbq+rQnN+sFj47auLblR5ka6aHNGPDgv8G/iI2Grb0ldQ==", + "dev": true, + "requires": { + "core-js-pure": "^3.20.2", + "regenerator-runtime": "^0.13.4" + } + }, "@babel/template": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.6.tgz", @@ -1255,10 +1265,114 @@ } }, "@bigcommerce/eslint-config": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@bigcommerce/eslint-config/-/eslint-config-1.0.1.tgz", - "integrity": "sha512-UfO6eovtvYxghaCKfogoIcdvIO46zpHEATLQjcNjbqswzuP2ajPIBMhraiXyly44QNp1kbu9p0qPozEbdKXDdQ==", - "dev": true + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@bigcommerce/eslint-config/-/eslint-config-2.6.1.tgz", + "integrity": "sha512-mJqWwWV/ZRJTUNhMq2WQU5eFIbex81vmy8HWT7T2mppyTceB4XY8WMPwrEfldLwynRjsF6ekTXTGC6/92/sgrA==", + "dev": true, + "requires": { + "@bigcommerce/eslint-plugin": "^1.1.1", + "@rushstack/eslint-patch": "^1.1.3", + "@typescript-eslint/eslint-plugin": "^5.22.0", + "@typescript-eslint/parser": "^5.22.0", + "eslint-config-prettier": "^8.5.0", + "eslint-import-resolver-typescript": "^2.7.1", + "eslint-plugin-gettext": "^1.2.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-jest": "^26.1.5", + "eslint-plugin-jest-formatting": "^3.1.0", + "eslint-plugin-jsdoc": "^39.2.9", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-react": "^7.29.4", + "eslint-plugin-react-hooks": "^4.5.0", + "eslint-plugin-switch-case": "^1.1.2", + "prettier": "^2.6.2" + } + }, + "@bigcommerce/eslint-plugin": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@bigcommerce/eslint-plugin/-/eslint-plugin-1.1.1.tgz", + "integrity": "sha512-N57eht9b0H63hvh6K4HTYsB8PypNdDb8eJzHuh9WmMerHd9CLrf9b/guIeMENOgqfoXmGrd6EE2cAuqmyIa3WQ==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "^5.22.0", + "tsutils": "^3.21.0" + }, + "dependencies": { + "@typescript-eslint/experimental-utils": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.37.0.tgz", + "integrity": "sha512-mmzzOOK2YpwSgzhXpeSAtAlxBZVLGuq8OdvrfzibR4jfTTrTd3AjCy17M2dUKVFNsrNfLM0nWsxMsJz0kiYHqw==", + "dev": true, + "requires": { + "@typescript-eslint/utils": "5.37.0" + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.37.0.tgz", + "integrity": "sha512-F67MqrmSXGd/eZnujjtkPgBQzgespu/iCZ+54Ok9X5tALb9L2v3G+QBSoWkXG0p3lcTJsL+iXz5eLUEdSiJU9Q==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.37.0", + "@typescript-eslint/visitor-keys": "5.37.0" + } + }, + "@typescript-eslint/types": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.37.0.tgz", + "integrity": "sha512-3frIJiTa5+tCb2iqR/bf7XwU20lnU05r/sgPJnRpwvfZaqCJBrl8Q/mw9vr3NrNdB/XtVyMA0eppRMMBqdJ1bA==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.37.0.tgz", + "integrity": "sha512-JkFoFIt/cx59iqEDSgIGnQpCTRv96MQnXCYvJi7QhBC24uyuzbD8wVbajMB1b9x4I0octYFJ3OwjAwNqk1AjDA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.37.0", + "@typescript-eslint/visitor-keys": "5.37.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/utils": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.37.0.tgz", + "integrity": "sha512-jUEJoQrWbZhmikbcWSMDuUSxEE7ID2W/QCV/uz10WtQqfOuKZUqFGjqLJ+qhDd17rjgp+QJPqTdPIBWwoob2NQ==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.37.0", + "@typescript-eslint/types": "5.37.0", + "@typescript-eslint/typescript-estree": "5.37.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.37.0.tgz", + "integrity": "sha512-Hp7rT4cENBPIzMwrlehLW/28EVCOcE9U1Z1BQTc8EA8v5qpr7GRGuG+U58V5tTY48zvUOA3KHvw3rA8tY9fbdA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.37.0", + "eslint-visitor-keys": "^3.3.0" + } + }, + "semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } }, "@bigcommerce/form-poster": { "version": "1.4.0", @@ -1375,6 +1489,17 @@ "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "dev": true }, + "@es-joy/jsdoccomment": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.31.0.tgz", + "integrity": "sha512-tc1/iuQcnaiSIUVad72PBierDFpsxdUHtEF/OrfqvM1CBAsIoMP51j52jTMb3dXriwhieTo289InzZj72jL3EQ==", + "dev": true, + "requires": { + "comment-parser": "1.3.1", + "esquery": "^1.4.0", + "jsdoc-type-pratt-parser": "~3.1.0" + } + }, "@eslint/eslintrc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", @@ -3728,6 +3853,12 @@ "url-parse": "^1.5.3" } }, + "@rushstack/eslint-patch": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.4.tgz", + "integrity": "sha512-LwzQKA4vzIct1zNZzBmRKI9QuNpLgTQMEjsQLf3BXuGYb3QPTP4Yjf6mkdX+X1mYttZ808QpOwAzZjv28kq7DA==", + "dev": true + }, "@sentry/browser": { "version": "7.11.1", "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.11.1.tgz", @@ -5332,6 +5463,16 @@ "sprintf-js": "~1.0.2" } }, + "aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" + } + }, "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -5486,6 +5627,12 @@ "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", "dev": true }, + "ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==", + "dev": true + }, "astral-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", @@ -5557,6 +5704,18 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, + "axe-core": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.4.3.tgz", + "integrity": "sha512-32+ub6kkdhhWick/UjvEwRchgoetXqTK14INLqbGm5U2TzBkBNF3nQtLYm8ovxSkQWArjEQvftCKryjZaATu3w==", + "dev": true + }, + "axobject-query": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", + "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==", + "dev": true + }, "babel-jest": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", @@ -6976,6 +7135,12 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, + "comment-parser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.1.tgz", + "integrity": "sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==", + "dev": true + }, "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -7460,6 +7625,12 @@ } } }, + "core-js-pure": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.25.1.tgz", + "integrity": "sha512-7Fr74bliUDdeJCBMxkkIuQ4xfxn/SwrVg+HkJUAoNEXVqYLv55l6Af0dJ5Lq2YBUW9yKqSkLXaS5SYPK6MGa/A==", + "dev": true + }, "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -7689,6 +7860,12 @@ "bin-wrapper": "^4.0.1" } }, + "damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true + }, "dargs": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", @@ -8822,6 +8999,35 @@ } } }, + "eslint-import-resolver-typescript": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.7.1.tgz", + "integrity": "sha512-00UbgGwV8bSgUv34igBDbTOtKhqoRMy9bFjNehT40bXg6585PNIct8HhXZ0SybqB9rWtXj9crcku8ndDn/gIqQ==", + "dev": true, + "requires": { + "debug": "^4.3.4", + "glob": "^7.2.0", + "is-glob": "^4.0.3", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, "eslint-module-utils": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz", @@ -8892,6 +9098,15 @@ } } }, + "eslint-plugin-gettext": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-gettext/-/eslint-plugin-gettext-1.2.0.tgz", + "integrity": "sha512-k7jSJD1nETfGYk94VS7AxHXPe7AZN7LvTfhBEnpyUx6DR98/5Bffovuw5vGk4axFBgKqzzfIV7ASTz/lnVd2qQ==", + "dev": true, + "requires": { + "gettext-parser": "^4.0.4" + } + }, "eslint-plugin-import": { "version": "2.26.0", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", @@ -8939,6 +9154,100 @@ } } }, + "eslint-plugin-jest": { + "version": "26.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-26.9.0.tgz", + "integrity": "sha512-TWJxWGp1J628gxh2KhaH1H1paEdgE2J61BBF1I59c6xWeL5+D1BzMxGDN/nXAfX+aSkR5u80K+XhskK6Gwq9ng==", + "dev": true, + "requires": { + "@typescript-eslint/utils": "^5.10.0" + } + }, + "eslint-plugin-jest-formatting": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest-formatting/-/eslint-plugin-jest-formatting-3.1.0.tgz", + "integrity": "sha512-XyysraZ1JSgGbLSDxjj5HzKKh0glgWf+7CkqxbTqb7zEhW7X2WHo5SBQ8cGhnszKN+2Lj3/oevBlHNbHezoc/A==", + "dev": true + }, + "eslint-plugin-jsdoc": { + "version": "39.3.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.3.6.tgz", + "integrity": "sha512-R6dZ4t83qPdMhIOGr7g2QII2pwCjYyKP+z0tPOfO1bbAbQyKC20Y2Rd6z1te86Lq3T7uM8bNo+VD9YFpE8HU/g==", + "dev": true, + "requires": { + "@es-joy/jsdoccomment": "~0.31.0", + "comment-parser": "1.3.1", + "debug": "^4.3.4", + "escape-string-regexp": "^4.0.0", + "esquery": "^1.4.0", + "semver": "^7.3.7", + "spdx-expression-parse": "^3.0.1" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "eslint-plugin-jsx-a11y": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz", + "integrity": "sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q==", + "dev": true, + "requires": { + "@babel/runtime": "^7.18.9", + "aria-query": "^4.2.2", + "array-includes": "^3.1.5", + "ast-types-flow": "^0.0.7", + "axe-core": "^4.4.3", + "axobject-query": "^2.2.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "has": "^1.0.3", + "jsx-ast-utils": "^3.3.2", + "language-tags": "^1.0.5", + "minimatch": "^3.1.2", + "semver": "^6.3.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.0.tgz", + "integrity": "sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + } + } + }, + "eslint-plugin-prettier": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", + "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, "eslint-plugin-react": { "version": "7.30.1", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.30.1.tgz", @@ -8989,6 +9298,16 @@ "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", "dev": true }, + "eslint-plugin-switch-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-switch-case/-/eslint-plugin-switch-case-1.1.2.tgz", + "integrity": "sha512-mhDdJ6WX5LKv0PccziefBGPhIryJamgd3vTNqhEZUBeTGUeGdsgttwU/68xOViyScwr8RqCwTGC2Pd1cPYGNRg==", + "dev": true, + "requires": { + "lodash.last": "^3.0.0", + "lodash.zipobject": "^4.0.0" + } + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -9566,6 +9885,12 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, "fast-glob": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", @@ -10279,6 +10604,26 @@ "assert-plus": "^1.0.0" } }, + "gettext-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-4.2.0.tgz", + "integrity": "sha512-aMgPyjC9W5Mz9tbFU8DcQ7GYMXoFWq633kaWGt4imlcpBWzDIWk7HY7nCSZTCJxyjRaLq9L/NEjMKkZ9gR630Q==", + "dev": true, + "requires": { + "content-type": "^1.0.4", + "encoding": "^0.1.13", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, "gifsicle": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/gifsicle/-/gifsicle-5.3.0.tgz", @@ -14144,6 +14489,12 @@ "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "dev": true }, + "jsdoc-type-pratt-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-3.1.0.tgz", + "integrity": "sha512-MgtD0ZiCDk9B+eI73BextfRrVQl0oyzRG8B2BjORts6jbunj4ScKPcyXGTbB6eXL4y9TzxCm6hyeLq/2ASzNdw==", + "dev": true + }, "jsdom": { "version": "16.7.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", @@ -14318,6 +14669,21 @@ "integrity": "sha512-P+0a/gBzLgVlCnK8I7VcD0yuYJscmWn66wH9tlKsQnmVdg689tLEmziwB9PuazZYLkcm07fvWOKCJJqI55sD5Q==", "dev": true }, + "language-subtag-registry": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", + "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==", + "dev": true + }, + "language-tags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", + "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==", + "dev": true, + "requires": { + "language-subtag-registry": "~0.3.2" + } + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -14495,6 +14861,12 @@ "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", "dev": true }, + "lodash.last": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash.last/-/lodash.last-3.0.0.tgz", + "integrity": "sha512-14mq7rSkCxG4XMy9lF2FbIOqqgF0aH0NfPuQ3LPR3vIh0kHnUvIYP70dqa1Hf47zyXfQ8FzAg0MYOQeSuE1R7A==", + "dev": true + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -14530,6 +14902,12 @@ "lodash._reinterpolate": "^3.0.0" } }, + "lodash.zipobject": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lodash.zipobject/-/lodash.zipobject-4.1.3.tgz", + "integrity": "sha512-A9SzX4hMKWS25MyalwcOnNoplyHbkNVsjidhTp8ru0Sj23wY9GWBKS8gAIGDSAqeWjIjvE4KBEl24XXAs+v4wQ==", + "dev": true + }, "log-symbols": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", @@ -16697,6 +17075,21 @@ "dev": true, "optional": true }, + "prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "dev": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, "pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", diff --git a/package.json b/package.json index f674cad530..13d87a308d 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "release:alpha": "npm run lint && npm run test -- --coverage && npm run build -- --env prerelease=prerelease && npm run release:version", "release:version": "git add dist && standard-version -a", "generate": "npx nx run core:generate", - "lint": "npx nx run-many --target=lint --all", + "lint": "npx nx run-many --target=lint --all --fix", "test": "npx nx run-many --target=test --all", "test:watch": "npx nx run-many --target=test --all --watch" }, @@ -32,6 +32,7 @@ "bugs": { "url": "https://github.com/bigcommerce/checkout-js/issues" }, + "prettier": "@bigcommerce/eslint-config/prettier", "homepage": "https://github.com/bigcommerce/checkout-js#readme", "dependencies": { "@bigcommerce/checkout-sdk": "^1.287.1", @@ -83,7 +84,7 @@ "devDependencies": { "@babel/core": "^7.5.5", "@babel/preset-env": "^7.5.5", - "@bigcommerce/eslint-config": "^1.0.1", + "@bigcommerce/eslint-config": "^2.6.1", "@faker-js/faker": "^6.3.1", "@nrwl/cli": "^13.10.3", "@nrwl/devkit": "^13.10.3", @@ -132,6 +133,7 @@ "node-sass": "^4.14.1", "nx": "^13.10.3", "polly-adapter-playwright": "^2.1.0", + "prettier": "^2.7.1", "react-test-renderer": "^16.8.6", "sass-loader": "^7.1.0", "semver": "^6.3.0", diff --git a/packages/afterpay-integration/e2e/Afterpay.spec.ts b/packages/afterpay-integration/e2e/Afterpay.spec.ts index 347e8e2ae0..9cfbc9baf0 100644 --- a/packages/afterpay-integration/e2e/Afterpay.spec.ts +++ b/packages/afterpay-integration/e2e/Afterpay.spec.ts @@ -1,7 +1,11 @@ -import { test, PaymentStepAsGuestPreset, UseAUDPreset } from '@bigcommerce/checkout/payment-integration-test-framework'; +import { + PaymentStepAsGuestPreset, + test, + UseAUDPreset, +} from '@bigcommerce/checkout/payment-integration-test-framework'; test.describe('Sample Test Group', () => { - test('Can see Afterpay AU on the payment step', async ({assertions, checkout}) => { + test('Can see Afterpay AU on the payment step', async ({ assertions, checkout }) => { // Testing environment setup await checkout.use(new UseAUDPreset('AUD')); await checkout.use(new PaymentStepAsGuestPreset()); diff --git a/packages/afterpay-integration/src/lib/afterpay-integration.spec.ts b/packages/afterpay-integration/src/lib/afterpay-integration.spec.ts index 94ca62ae78..eac1b37110 100644 --- a/packages/afterpay-integration/src/lib/afterpay-integration.spec.ts +++ b/packages/afterpay-integration/src/lib/afterpay-integration.spec.ts @@ -2,6 +2,6 @@ import { afterpayIntegration } from './afterpay-integration'; describe('afterpayIntegration', () => { it('should work', () => { - expect(afterpayIntegration()).toEqual('afterpay-integration'); - }) -}) \ No newline at end of file + expect(afterpayIntegration()).toBe('afterpay-integration'); + }); +}); diff --git a/packages/apple-pay-integration/.eslintrc.json b/packages/apple-pay-integration/.eslintrc.json index c2594e335d..128cd29f1b 100644 --- a/packages/apple-pay-integration/.eslintrc.json +++ b/packages/apple-pay-integration/.eslintrc.json @@ -1,3 +1,5 @@ { - "extends": ["../../.eslintrc.json"] + "extends": [ + "../../.eslintrc.json" + ] } diff --git a/packages/apple-pay-integration/e2e/ApplePaySessionCustomerStepMockObject.ts b/packages/apple-pay-integration/e2e/ApplePaySessionCustomerStepMockObject.ts index fce06b45e9..1741911bdc 100644 --- a/packages/apple-pay-integration/e2e/ApplePaySessionCustomerStepMockObject.ts +++ b/packages/apple-pay-integration/e2e/ApplePaySessionCustomerStepMockObject.ts @@ -1,61 +1,78 @@ const addApplePaySessionToChrome = () => { class ApplePaySessionCustomerStep implements ApplePaySession { + // eslint-disable-next-line @typescript-eslint/naming-convention + static STATUS_FAILURE = 2; + // eslint-disable-next-line @typescript-eslint/naming-convention + static STATUS_SUCCESS = 1; + version: number; paymentRequest: ApplePayJS.ApplePayPaymentRequest; - static STATUS_SUCCESS = 1; - static STATUS_FAILURE = 2; - static supportsVersion(versionNumber: number) { - console.log('supportsVersion', versionNumber) + console.log('supportsVersion', versionNumber); + return true; } - constructor(version, paymentRequest) { + // eslint-disable-next-line @typescript-eslint/member-ordering + constructor(version: number, paymentRequest: ApplePayJS.ApplePayPaymentRequest) { this.version = version; this.paymentRequest = paymentRequest; } - addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions | undefined): void { + addEventListener( + type: string, + callback: EventListenerOrEventListenerObject | null, + options?: boolean | AddEventListenerOptions | undefined, + ): void { console.log('addEventListener', type, callback, options); } dispatchEvent(event: Event): boolean { console.log('dispatchEvent', event); - return true + + return true; } - removeEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions | undefined): void { + removeEventListener( + type: string, + callback: EventListenerOrEventListenerObject | null, + options?: boolean | EventListenerOptions | undefined, + ): void { console.log('removeEventListener', type, callback, options); } - oncancel: (event: ApplePayJS.Event) => void = () => { + oncancel: (event: ApplePayJS.Event) => void = () => { console.log('oncancel'); - } + }; onpaymentauthorized: (event: ApplePayJS.ApplePayPaymentAuthorizedEvent) => void = () => { console.log('onpaymentauthorized'); - } + }; - onpaymentmethodselected: (event: ApplePayJS.ApplePayPaymentMethodSelectedEvent) => void = () => { - console.log('onpaymentmethodselected'); - } + onpaymentmethodselected: (event: ApplePayJS.ApplePayPaymentMethodSelectedEvent) => void = + () => { + console.log('onpaymentmethodselected'); + }; - onshippingcontactselected: (event: ApplePayJS.ApplePayShippingContactSelectedEvent) => void = () => { + onshippingcontactselected: ( + event: ApplePayJS.ApplePayShippingContactSelectedEvent, + ) => void = () => { console.log('onshippingcontactselected'); - } + }; - onshippingmethodselected: (event: ApplePayJS.ApplePayShippingMethodSelectedEvent) => void = () => { - console.log('onshippingmethodselected'); - } + onshippingmethodselected: (event: ApplePayJS.ApplePayShippingMethodSelectedEvent) => void = + () => { + console.log('onshippingmethodselected'); + }; - onvalidatemerchant: (event: ApplePayJS.ApplePayValidateMerchantEvent) => void = () => { + onvalidatemerchant: (event: ApplePayJS.ApplePayValidateMerchantEvent) => void = () => { console.log('onvalidatemerchant'); - } + }; abort(): void { - console.log('abort') - } + console.log('abort'); + } canMakePayments() { return true; @@ -81,32 +98,42 @@ const addApplePaySessionToChrome = () => { console.log('completeMerchantValidation', response); } - completePaymentMethodSelection(...args: [newTotal: ApplePayJS.ApplePayLineItem, newLineItems: ApplePayJS.ApplePayLineItem[]] | [update: ApplePayJS.ApplePayPaymentMethodUpdate]): void { + completePaymentMethodSelection( + ...args: + | [ + newTotal: ApplePayJS.ApplePayLineItem, + newLineItems: ApplePayJS.ApplePayLineItem[], + ] + | [update: ApplePayJS.ApplePayPaymentMethodUpdate] + ): void { console.log('completeMerchantValidation', args); } begin() { setTimeout(() => { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const validationEvent = { validationURL: 'https://www.example.com', - } as ApplePayJS.ApplePayValidateMerchantEvent + } as ApplePayJS.ApplePayValidateMerchantEvent; + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const shippingContactEvent = { shippingContact: { - email: "xx@xx.com", + email: 'xx@xx.com', familyName: 'xx', - givenName: 'xx' - } + givenName: 'xx', + }, } as unknown as ApplePayJS.ApplePayShippingContactSelectedEvent; + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const shippingMethodSelectedEvent = { shippingMethod: { label: 'xx', detail: 'xx', - amount: "xx", - identifier: 'xx' - } - } as ApplePayJS.ApplePayShippingMethodSelectedEvent + amount: 'xx', + identifier: 'xx', + }, + } as ApplePayJS.ApplePayShippingMethodSelectedEvent; this.onvalidatemerchant(validationEvent); this.onshippingcontactselected(shippingContactEvent); @@ -114,15 +141,16 @@ const addApplePaySessionToChrome = () => { }, 0); setTimeout(() => { const mockPaymentData = { - version: "mock_v1", - data: "mockData", - signature: "mockSignature", + version: 'mock_v1', + data: 'mockData', + signature: 'mockSignature', header: { - ephemeralPublicKey: "mockPublicKey", - publicKeyHash: "mockPublicKeyHash", - transactionId: "mockTransactionId", - } - } + ephemeralPublicKey: 'mockPublicKey', + publicKeyHash: 'mockPublicKeyHash', + transactionId: 'mockTransactionId', + }, + }; + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const event = { payment: { token: { @@ -134,32 +162,33 @@ const addApplePaySessionToChrome = () => { paymentPass: { primaryAccountIdentifier: 'xx', primaryAccountNumberSuffix: 'xx', - activationState: 'activated' - } + activationState: 'activated', + }, }, - transactionIdentifier: "xx", + transactionIdentifier: 'xx', }, billingContact: { - emailAddress: "mock@mock.com", - familyName: "mock", - givenName: "mock", - phoneNumber: "00000000", + emailAddress: 'mock@mock.com', + familyName: 'mock', + givenName: 'mock', + phoneNumber: '00000000', }, shippingContact: { - emailAddress: "mock@mock.com", - familyName: "mock", - givenName: "mock", - phoneNumber: "00000000", - } + emailAddress: 'mock@mock.com', + familyName: 'mock', + givenName: 'mock', + phoneNumber: '00000000', + }, }, } as unknown as ApplePayJS.ApplePayPaymentAuthorizedEvent; this.onpaymentauthorized(event); - }, 1000) + }, 1000); } } - window['ApplePaySession'] = ApplePaySessionCustomerStep; -} + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + window.ApplePaySession = ApplePaySessionCustomerStep as unknown as ApplePaySession; +}; -export default addApplePaySessionToChrome +export default addApplePaySessionToChrome; diff --git a/packages/apple-pay-integration/e2e/ApplePaySessionPaymentStepMockObject.ts b/packages/apple-pay-integration/e2e/ApplePaySessionPaymentStepMockObject.ts index 0ca3e87621..828d9753b7 100644 --- a/packages/apple-pay-integration/e2e/ApplePaySessionPaymentStepMockObject.ts +++ b/packages/apple-pay-integration/e2e/ApplePaySessionPaymentStepMockObject.ts @@ -1,13 +1,15 @@ const addApplePaySessionToChrome = () => { class ApplePaySessionPaymentStep implements ApplePaySession { + // eslint-disable-next-line @typescript-eslint/naming-convention + static STATUS_FAILURE = 2; + // eslint-disable-next-line @typescript-eslint/naming-convention + static STATUS_SUCCESS = 1; version: number; paymentRequest: ApplePayJS.ApplePayPaymentRequest; - static STATUS_SUCCESS = 1; - static STATUS_FAILURE = 2; - static supportsVersion(versionNumber: number) { - console.log('supportsVersion', versionNumber) + console.log('supportsVersion', versionNumber); + return true; } @@ -19,52 +21,66 @@ const addApplePaySessionToChrome = () => { return Promise.resolve(this.canMakePayments()); } - constructor(version, paymentRequest) { + // eslint-disable-next-line @typescript-eslint/member-ordering + constructor(version: number, paymentRequest: ApplePayJS.ApplePayPaymentRequest) { this.version = version; this.paymentRequest = paymentRequest; } - addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions | undefined): void { + addEventListener( + type: string, + callback: EventListenerOrEventListenerObject | null, + options?: boolean | AddEventListenerOptions | undefined, + ): void { console.log('addEventListener', type, callback, options); } dispatchEvent(event: Event): boolean { console.log('dispatchEvent', event); - return true + + return true; } - removeEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions | undefined): void { + removeEventListener( + type: string, + callback: EventListenerOrEventListenerObject | null, + options?: boolean | EventListenerOptions | undefined, + ): void { console.log('removeEventListener', type, callback, options); } - oncancel: (event: ApplePayJS.Event) => void = () => { + oncancel: (event: ApplePayJS.Event) => void = () => { console.log('oncancel'); - } + }; onpaymentauthorized: (event: ApplePayJS.ApplePayPaymentAuthorizedEvent) => void = () => { console.log('onpaymentauthorized'); - } + }; - onpaymentmethodselected: (event: ApplePayJS.ApplePayPaymentMethodSelectedEvent) => void = () => { - console.log('onpaymentmethodselected'); - } + onpaymentmethodselected: (event: ApplePayJS.ApplePayPaymentMethodSelectedEvent) => void = + () => { + console.log('onpaymentmethodselected'); + }; - onshippingcontactselected: (event: ApplePayJS.ApplePayShippingContactSelectedEvent) => void = () => { + onshippingcontactselected: ( + event: ApplePayJS.ApplePayShippingContactSelectedEvent, + ) => void = () => { console.log('onshippingcontactselected'); - } + }; - onshippingmethodselected: (event: ApplePayJS.ApplePayShippingMethodSelectedEvent) => void = () => { - console.log('onshippingmethodselected'); - } + onshippingmethodselected: (event: ApplePayJS.ApplePayShippingMethodSelectedEvent) => void = + () => { + console.log('onshippingmethodselected'); + }; - onvalidatemerchant: (event: ApplePayJS.ApplePayValidateMerchantEvent) => void = () => { + onvalidatemerchant: (event: ApplePayJS.ApplePayValidateMerchantEvent) => void = () => { console.log('onvalidatemerchant'); - } + }; abort(): void { - console.log('abort') - } - + console.log('abort'); + } + completePayment() { console.log('completePayment'); } @@ -81,29 +97,38 @@ const addApplePaySessionToChrome = () => { console.log('completeMerchantValidation', response); } - completePaymentMethodSelection(...args: [newTotal: ApplePayJS.ApplePayLineItem, newLineItems: ApplePayJS.ApplePayLineItem[]] | [update: ApplePayJS.ApplePayPaymentMethodUpdate]): void { + completePaymentMethodSelection( + ...args: + | [ + newTotal: ApplePayJS.ApplePayLineItem, + newLineItems: ApplePayJS.ApplePayLineItem[], + ] + | [update: ApplePayJS.ApplePayPaymentMethodUpdate] + ): void { console.log('completeMerchantValidation', args); } begin() { setTimeout(() => { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const event = { validationURL: 'https://www.example.com', - } as ApplePayJS.ApplePayValidateMerchantEvent + } as ApplePayJS.ApplePayValidateMerchantEvent; this.onvalidatemerchant(event); }, 0); setTimeout(() => { const mockPaymentData = { - version: "mock_v1", - data: "mockData", - signature: "mockSignature", + version: 'mock_v1', + data: 'mockData', + signature: 'mockSignature', header: { - ephemeralPublicKey: "mockPublicKey", - publicKeyHash: "mockPublicKeyHash", - transactionId: "mockTransactionId", - } - } + ephemeralPublicKey: 'mockPublicKey', + publicKeyHash: 'mockPublicKeyHash', + transactionId: 'mockTransactionId', + }, + }; + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const event = { payment: { token: { @@ -115,32 +140,33 @@ const addApplePaySessionToChrome = () => { paymentPass: { primaryAccountIdentifier: 'xx', primaryAccountNumberSuffix: 'xx', - activationState: 'activated' - } + activationState: 'activated', + }, }, - transactionIdentifier: "xx", + transactionIdentifier: 'xx', }, billingContact: { - emailAddress: "mock@mock.com", - familyName: "mock", - givenName: "mock", - phoneNumber: "00000000", + emailAddress: 'mock@mock.com', + familyName: 'mock', + givenName: 'mock', + phoneNumber: '00000000', }, shippingContact: { - emailAddress: "mock@mock.com", - familyName: "mock", - givenName: "mock", - phoneNumber: "00000000", - } + emailAddress: 'mock@mock.com', + familyName: 'mock', + givenName: 'mock', + phoneNumber: '00000000', + }, }, } as unknown as ApplePayJS.ApplePayPaymentAuthorizedEvent; this.onpaymentauthorized(event); - }, 1000) + }, 1000); } } - window['ApplePaySession'] = ApplePaySessionPaymentStep; -} + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + window.ApplePaySession = ApplePaySessionPaymentStep as unknown as ApplePaySession; +}; export default addApplePaySessionToChrome; diff --git a/packages/apple-pay-integration/e2e/ApplePayTestMockResponse.ts b/packages/apple-pay-integration/e2e/ApplePayTestMockResponse.ts index 47e0314f72..88aadfdcf3 100644 --- a/packages/apple-pay-integration/e2e/ApplePayTestMockResponse.ts +++ b/packages/apple-pay-integration/e2e/ApplePayTestMockResponse.ts @@ -44,7 +44,7 @@ export const applePayCart = `{ }, "clientToken":null, "returnUrl":null - }` + }`; export const applepay = `{ "id": "applepay", @@ -246,8 +246,8 @@ export const order = `{ "payments": [] }`; - -export const orderPayment = '{"status":"ok","three_ds_result":{"acs_url":null,"payer_auth_request":null,"merchant_data":null,"callback_url":null},"errors":[]}'; +export const orderPayment = + '{"status":"ok","three_ds_result":{"acs_url":null,"payer_auth_request":null,"merchant_data":null,"callback_url":null},"errors":[]}'; export const internalOrder = `{ "data": { diff --git a/packages/apple-pay-integration/e2e/applepay.spec.ts b/packages/apple-pay-integration/e2e/applepay.spec.ts index af34d2a857..cc1cfde30e 100644 --- a/packages/apple-pay-integration/e2e/applepay.spec.ts +++ b/packages/apple-pay-integration/e2e/applepay.spec.ts @@ -1,26 +1,58 @@ -import { test, CustomerStepPreset, PaymentStepAsGuestPreset } from '@bigcommerce/checkout/payment-integration-test-framework'; -import { applePayCart, internalOrder, order, orderPayment, validateMerchantResponse, consignmentsAndBilling } from './ApplePayTestMockResponse'; -import addApplePaySessionToChromePaymentStep from './ApplePaySessionPaymentStepMockObject' -import addApplePaySessionToChromeCustomerStep from './ApplePaySessionCustomerStepMockObject' +import { + CustomerStepPreset, + PaymentStepAsGuestPreset, + test, +} from '@bigcommerce/checkout/payment-integration-test-framework'; + +import addApplePaySessionToChromeCustomerStep from './ApplePaySessionCustomerStepMockObject'; +import addApplePaySessionToChromePaymentStep from './ApplePaySessionPaymentStepMockObject'; +import { + applePayCart, + consignmentsAndBilling, + internalOrder, + order, + orderPayment, + validateMerchantResponse, +} from './ApplePayTestMockResponse'; test.describe('ApplePay', () => { - test('Customer should be able to pay using ApplePay through the payment step in checkout', async ({ assertions, checkout, page }) => { + test('Customer should be able to pay using ApplePay through the payment step in checkout', async ({ + assertions, + checkout, + page, + }) => { // Testing environment setup await page.addInitScript(addApplePaySessionToChromePaymentStep); const responseProps = { status: 200, contentType: 'application/json' }; + await checkout.use(new PaymentStepAsGuestPreset()); - await checkout.start('ApplePay in Payment Step') - await checkout.route(/order-confirmation.*/, './packages/payment-integration-test-framework/src/support/orderConfirmation.ejs', { orderId: '124' }); - await page.route('**/api/storefront/payments/applepay?cartId=124', route => route.fulfill({ ...responseProps, body: applePayCart })); - await page.route('**/api/public/v1/payments/applepay/validate_merchant', route => route.fulfill({ ...responseProps, body: validateMerchantResponse })); - await page.route(/.*\/api\/storefront\/orders\/124.*/, route => route.fulfill({ ...responseProps, body: order })); - await page.route('**/api/public/v1/orders/payments', route => route.fulfill({ ...responseProps, body: orderPayment })); - await page.route('**/internalapi/v1/checkout/order', route => route.fulfill({ - ...responseProps, - headers: { Token: 'White shirt now red, my bloody nose' }, - body: internalOrder, - })); + await checkout.start('ApplePay in Payment Step'); + await checkout.route( + /order-confirmation.*/, + './packages/payment-integration-test-framework/src/support/orderConfirmation.ejs', + { orderId: '124' }, + ); + await page.route('**/api/storefront/payments/applepay?cartId=124', (route) => { + void route.fulfill({ ...responseProps, body: applePayCart }); + }); + await page.route('**/api/public/v1/payments/applepay/validate_merchant', (route) => { + void route.fulfill({ ...responseProps, body: validateMerchantResponse }); + }); + await page.route(/.*\/api\/storefront\/orders\/124.*/, (route) => { + void route.fulfill({ ...responseProps, body: order }); + }); + await page.route('**/api/public/v1/orders/payments', (route) => { + void route.fulfill({ ...responseProps, body: orderPayment }); + }); + await page.route('**/internalapi/v1/checkout/order', (route) => { + void route.fulfill({ + ...responseProps, + // eslint-disable-next-line @typescript-eslint/naming-convention + headers: { Token: 'White shirt now red, my bloody nose' }, + body: internalOrder, + }); + }); // Playwright actions await checkout.goto(); @@ -29,32 +61,56 @@ test.describe('ApplePay', () => { // Assertions await assertions.shouldSeeOrderConfirmation(); - }) + }); - test('Customer should be able to pay using ApplePay through the customer step in checkout', async ({ assertions, checkout, page }) => { + test('Customer should be able to pay using ApplePay through the customer step in checkout', async ({ + assertions, + checkout, + page, + }) => { // Testing environment setup await page.addInitScript(addApplePaySessionToChromeCustomerStep); const responseProps = { status: 200, contentType: 'application/json' }; + await checkout.use(new CustomerStepPreset()); await checkout.start('ApplePay in Customer Step'); - await checkout.route(/order-confirmation.*/, './packages/payment-integration-test-framework/src/support/orderConfirmation.ejs', { orderId: '124' }); - await page.route('**/api/public/v1/payments/applepay/validate_merchant', route => route.fulfill({ ...responseProps, body: validateMerchantResponse })); - await page.route(/.*\/api\/storefront\/orders\/124.*/, route => route.fulfill({ ...responseProps, body: order })); - await page.route('**/api/public/v1/orders/payments', route => route.fulfill({ ...responseProps, body: orderPayment })); - await page.route(/.*\/api\/storefront\/checkout\/124.*\//, route => route.fulfill({ ...responseProps, body: consignmentsAndBilling })); - await page.route(/.*\/api\/storefront\/checkouts\/124.*\/consignments/, route => route.fulfill({ ...responseProps, body: consignmentsAndBilling })); - await page.route(/.*\/api\/storefront\/checkouts\/124.*\/billing-address/, route => route.fulfill({ ...responseProps, body: consignmentsAndBilling })); - await page.route('**/internalapi/v1/checkout/order', route => route.fulfill({ - ...responseProps, - headers: { Token: 'White shirt now red, my bloody nose' }, - body: internalOrder, - })); + await checkout.route( + /order-confirmation.*/, + './packages/payment-integration-test-framework/src/support/orderConfirmation.ejs', + { orderId: '124' }, + ); + await page.route('**/api/public/v1/payments/applepay/validate_merchant', (route) => { + void route.fulfill({ ...responseProps, body: validateMerchantResponse }); + }); + await page.route(/.*\/api\/storefront\/orders\/124.*/, (route) => { + void route.fulfill({ ...responseProps, body: order }); + }); + await page.route('**/api/public/v1/orders/payments', (route) => { + void route.fulfill({ ...responseProps, body: orderPayment }); + }); + await page.route(/.*\/api\/storefront\/checkout\/124.*\//, (route) => { + void route.fulfill({ ...responseProps, body: consignmentsAndBilling }); + }); + await page.route(/.*\/api\/storefront\/checkouts\/124.*\/consignments/, (route) => { + void route.fulfill({ ...responseProps, body: consignmentsAndBilling }); + }); + await page.route(/.*\/api\/storefront\/checkouts\/124.*\/billing-address/, (route) => { + void route.fulfill({ ...responseProps, body: consignmentsAndBilling }); + }); + await page.route('**/internalapi/v1/checkout/order', (route) => { + void route.fulfill({ + ...responseProps, + // eslint-disable-next-line @typescript-eslint/naming-convention + headers: { Token: 'White shirt now red, my bloody nose' }, + body: internalOrder, + }); + }); // Playwright actions await checkout.goto(); await page.locator('[aria-label="Apple Pay"]').click(); // Assertions await assertions.shouldSeeOrderConfirmation(); - }) + }); }); diff --git a/packages/apple-pay-integration/src/ApplePayPaymentMethod.spec.tsx b/packages/apple-pay-integration/src/ApplePayPaymentMethod.spec.tsx index 56cc3b89ad..84701a8fac 100644 --- a/packages/apple-pay-integration/src/ApplePayPaymentMethod.spec.tsx +++ b/packages/apple-pay-integration/src/ApplePayPaymentMethod.spec.tsx @@ -1,7 +1,8 @@ -import { PaymentFormService } from "@bigcommerce/checkout/payment-integration-api"; -import { createCheckoutService, LanguageService } from "@bigcommerce/checkout-sdk"; +import { createCheckoutService, LanguageService } from '@bigcommerce/checkout-sdk'; import { mount } from 'enzyme'; -import React from "react"; +import React from 'react'; + +import { PaymentFormService } from '@bigcommerce/checkout/payment-integration-api'; import ApplePaymentMethod from './ApplePayPaymentMethod'; import { getMethod } from './paymentMethods.mock'; @@ -13,47 +14,57 @@ describe('ApplePay payment method', () => { method: getMethod(), checkoutService, checkoutState, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions paymentForm: jest.fn() as unknown as PaymentFormService, - language: {translate: jest.fn() } as unknown as LanguageService, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + language: { translate: jest.fn() } as unknown as LanguageService, onUnhandledError: jest.fn(), }; it('initializes ApplePay with required props', () => { - const initializePayment = jest.spyOn(checkoutService,'initializePayment').mockResolvedValue(checkoutState); + const initializePayment = jest + .spyOn(checkoutService, 'initializePayment') + .mockResolvedValue(checkoutState); const component = mount(); expect(component.find(ApplePaymentMethod)).toHaveLength(1); - expect(initializePayment).toHaveBeenCalled() + expect(initializePayment).toHaveBeenCalled(); }); it('deinitializes ApplePay with required props', () => { - const deinitializePayment = jest.spyOn(checkoutService,'deinitializePayment').mockResolvedValue(checkoutState); + const deinitializePayment = jest + .spyOn(checkoutService, 'deinitializePayment') + .mockResolvedValue(checkoutState); const component = mount(); + component.unmount(); expect(component.find(ApplePaymentMethod)).toHaveLength(0); - expect(deinitializePayment).toHaveBeenCalled() + expect(deinitializePayment).toHaveBeenCalled(); }); - it('catches error during ApplePay initialization', async () => { - jest.spyOn(checkoutService,'initializePayment').mockRejectedValue(new Error('test error')) + it('catches error during ApplePay initialization', async () => { + jest.spyOn(checkoutService, 'initializePayment').mockRejectedValue(new Error('test error')); mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); expect(props.onUnhandledError).toHaveBeenCalled(); - }) + }); + + it('catches error during ApplePay deinitialization', async () => { + jest.spyOn(checkoutService, 'deinitializePayment').mockRejectedValue( + new Error('test error'), + ); - it('catches error during ApplePay deinitialization', async () => { - jest.spyOn(checkoutService,'deinitializePayment').mockRejectedValue(new Error('test error')) const component = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.unmount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); expect(props.onUnhandledError).toHaveBeenCalled(); - }) + }); }); diff --git a/packages/apple-pay-integration/src/ApplePayPaymentMethod.tsx b/packages/apple-pay-integration/src/ApplePayPaymentMethod.tsx index 9b35ed971e..d18769d6be 100644 --- a/packages/apple-pay-integration/src/ApplePayPaymentMethod.tsx +++ b/packages/apple-pay-integration/src/ApplePayPaymentMethod.tsx @@ -1,7 +1,17 @@ -import { toResolvableComponent, PaymentMethodProps, PaymentMethodResolveId } from '@bigcommerce/checkout/payment-integration-api'; import React, { FunctionComponent, useEffect } from 'react'; -const ApplePaymentMethod: FunctionComponent = ({ method, checkoutService, language, onUnhandledError }) => { +import { + PaymentMethodProps, + PaymentMethodResolveId, + toResolvableComponent, +} from '@bigcommerce/checkout/payment-integration-api'; + +const ApplePaymentMethod: FunctionComponent = ({ + method, + checkoutService, + language, + onUnhandledError, +}) => { useEffect(() => { const initializePayment = async () => { try { @@ -16,10 +26,11 @@ const ApplePaymentMethod: FunctionComponent = ({ method, che } catch (error) { if (error instanceof Error) { onUnhandledError(error); - } + } } }; - initializePayment(); + + void initializePayment(); return () => { const deinitializePayment = async () => { @@ -34,14 +45,16 @@ const ApplePaymentMethod: FunctionComponent = ({ method, che } } }; - deinitializePayment(); - } + + void deinitializePayment(); + }; }, [checkoutService, language, method, onUnhandledError]); + // eslint-disable-next-line react/jsx-no-useless-fragment return <>; }; export default toResolvableComponent( ApplePaymentMethod, - [{ id: 'applepay' }] + [{ id: 'applepay' }], ); diff --git a/packages/apple-pay-integration/src/paymentMethods.mock.ts b/packages/apple-pay-integration/src/paymentMethods.mock.ts index c2f2e461c9..4c04acb474 100644 --- a/packages/apple-pay-integration/src/paymentMethods.mock.ts +++ b/packages/apple-pay-integration/src/paymentMethods.mock.ts @@ -1,4 +1,4 @@ -import { PaymentMethod } from "@bigcommerce/checkout-sdk"; +import { PaymentMethod } from '@bigcommerce/checkout-sdk'; export function getMethod(): PaymentMethod { return { @@ -6,15 +6,9 @@ export function getMethod(): PaymentMethod { gateway: undefined, logoUrl: '', method: 'credit-card', - supportedCards: [ - 'VISA', - 'AMEX', - 'MC', - ], + supportedCards: ['VISA', 'AMEX', 'MC'], initializationData: { - merchantCapabilities: [ - 'supports3DS', - ], + merchantCapabilities: ['supports3DS'], }, config: { displayName: 'Authorizenet', diff --git a/packages/apple-pay-integration/tsconfig.json b/packages/apple-pay-integration/tsconfig.json index 72723cd645..13ca601717 100644 --- a/packages/apple-pay-integration/tsconfig.json +++ b/packages/apple-pay-integration/tsconfig.json @@ -1,8 +1,11 @@ { "extends": "../../tsconfig.base.json", - "include": [ - "src/**/*.ts", - "src/**/*.tsx", - "types/*.d.ts" - ], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] } diff --git a/packages/apple-pay-integration/tsconfig.lib.json b/packages/apple-pay-integration/tsconfig.lib.json new file mode 100644 index 0000000000..8a7fff23df --- /dev/null +++ b/packages/apple-pay-integration/tsconfig.lib.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ] +} diff --git a/packages/apple-pay-integration/types/ApplePaySession.d.ts b/packages/apple-pay-integration/types/ApplePaySession.d.ts index fadfe32f40..9878257804 100644 --- a/packages/apple-pay-integration/types/ApplePaySession.d.ts +++ b/packages/apple-pay-integration/types/ApplePaySession.d.ts @@ -1,5 +1,6 @@ export declare global { - interface Window { - ApplePaySession?: ApplePaySession - } + interface Window { + // eslint-disable-next-line @typescript-eslint/naming-convention + ApplePaySession?: ApplePaySession; + } } diff --git a/packages/checkout-button-integration/src/CheckoutButton.spec.tsx b/packages/checkout-button-integration/src/CheckoutButton.spec.tsx index de24825241..d3caf0e8e2 100644 --- a/packages/checkout-button-integration/src/CheckoutButton.spec.tsx +++ b/packages/checkout-button-integration/src/CheckoutButton.spec.tsx @@ -1,8 +1,9 @@ -import { CheckoutButtonProps } from '@bigcommerce/checkout/payment-integration-api'; import { createCheckoutService, createLanguageService } from '@bigcommerce/checkout-sdk'; import { mount } from 'enzyme'; import React from 'react'; +import { CheckoutButtonProps } from '@bigcommerce/checkout/payment-integration-api'; + import CheckoutButton from './CheckoutButton'; describe('CheckoutButton', () => { @@ -24,48 +25,49 @@ describe('CheckoutButton', () => { it('initializes when component is mounted', () => { const { checkoutService, onUnhandledError } = defaultProps; - jest.spyOn(checkoutService, 'initializeCustomer') - .mockResolvedValue(checkoutService.getState()); + jest.spyOn(checkoutService, 'initializeCustomer').mockResolvedValue( + checkoutService.getState(), + ); - mount(); + mount(); - expect(checkoutService.initializeCustomer) - .toHaveBeenCalledWith({ - methodId: 'foobar', - foobar: { - container: 'button-container', - onUnhandledError, - }, - }); + expect(checkoutService.initializeCustomer).toHaveBeenCalledWith({ + methodId: 'foobar', + foobar: { + container: 'button-container', + onUnhandledError, + }, + }); }); it('deinitializes when component is unmounted', () => { const { checkoutService } = defaultProps; - jest.spyOn(checkoutService, 'initializeCustomer') - .mockResolvedValue(checkoutService.getState()); - jest.spyOn(checkoutService, 'deinitializeCustomer') - .mockResolvedValue(checkoutService.getState()); + jest.spyOn(checkoutService, 'initializeCustomer').mockResolvedValue( + checkoutService.getState(), + ); + jest.spyOn(checkoutService, 'deinitializeCustomer').mockResolvedValue( + checkoutService.getState(), + ); - const component = mount(); + const component = mount(); component.unmount(); - expect(checkoutService.deinitializeCustomer) - .toHaveBeenCalledWith({ - methodId: 'foobar', - }); + expect(checkoutService.deinitializeCustomer).toHaveBeenCalledWith({ + methodId: 'foobar', + }); }); it('renders empty container with provided ID', () => { const { checkoutService } = defaultProps; - jest.spyOn(checkoutService, 'initializeCustomer') - .mockResolvedValue(checkoutService.getState()); + jest.spyOn(checkoutService, 'initializeCustomer').mockResolvedValue( + checkoutService.getState(), + ); - const component = mount(); + const component = mount(); - expect(component.html()) - .toEqual('
'); + expect(component.html()).toBe('
'); }); }); diff --git a/packages/checkout-button-integration/src/CheckoutButton.tsx b/packages/checkout-button-integration/src/CheckoutButton.tsx index 6594f94e89..d08f509fcd 100644 --- a/packages/checkout-button-integration/src/CheckoutButton.tsx +++ b/packages/checkout-button-integration/src/CheckoutButton.tsx @@ -1,7 +1,11 @@ -import { CheckoutButtonProps, CheckoutButtonResolveId, toResolvableComponent } from '@bigcommerce/checkout/payment-integration-api'; - import React, { FunctionComponent, useEffect } from 'react'; +import { + CheckoutButtonProps, + CheckoutButtonResolveId, + toResolvableComponent, +} from '@bigcommerce/checkout/payment-integration-api'; + const CheckoutButton: FunctionComponent = ({ checkoutService: { deinitializeCustomer, initializeCustomer }, containerId, @@ -15,27 +19,16 @@ const CheckoutButton: FunctionComponent = ({ container: containerId, onUnhandledError, }, - }) - .catch(onUnhandledError); + }).catch(onUnhandledError); return () => { - deinitializeCustomer({ methodId }) - .catch(onUnhandledError); - } - }, [ - containerId, - deinitializeCustomer, - initializeCustomer, - methodId, - onUnhandledError, - ]); + deinitializeCustomer({ methodId }).catch(onUnhandledError); + }; + }, [containerId, deinitializeCustomer, initializeCustomer, methodId, onUnhandledError]); - return ( -
- ); -} + return
; +}; -export default toResolvableComponent( - CheckoutButton, - [{ default: true }] -); +export default toResolvableComponent(CheckoutButton, [ + { default: true }, +]); diff --git a/packages/core/.eslintrc.json b/packages/core/.eslintrc.json index f363e6c805..3459e6d458 100644 --- a/packages/core/.eslintrc.json +++ b/packages/core/.eslintrc.json @@ -1,56 +1,63 @@ { "extends": ["../../.eslintrc.json"], - "overrides": [ - { - "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "rules": { - "@bigcommerce/jsx-short-circuit-conditionals": "off", - "@typescript-eslint/ban-ts-comment": "warn", - "@typescript-eslint/no-inferrable-types": "warn", - "@typescript-eslint/no-unused-expressions": "error", - "@typescript-eslint/no-var-requires": "off", - "react/destructuring-assignment": "warn", - "react/jsx-curly-spacing": [ - "error", - { - "children": true, - "when": "always" - } - ], - "react/jsx-no-bind": "off", - "react-hooks/exhaustive-deps": "off", - "import/no-internal-modules": [ - "error", - { - "allow": [ - "react-dom/test-utils", - "scripts/*", - "**/*.{mock,scss}", - "**/index.ts", - "**/billing/Billing.tsx", - "**/cart/CartSummary.tsx", - "**/cart/CartSummaryDrawer.tsx", - "**/customer/Customer.tsx", - "**/order/OrderSummary.tsx", - "**/order/OrderSummaryDrawer.tsx", - "**/payment/Payment.tsx", - "**/shipping/Shipping.tsx", - "**/translations/*.json", - "**/locale/getLanguageService.ts", - "**/checkoutSuggestion/CheckoutSuggestion", - "**/order/renderOrderConfirmation", - "**/checkout/renderCheckout" - ] - } - ], - "no-var": "off", - "object-curly-newline": [ - "error", - { - "ImportDeclaration": "never" - } - ] - } - } - ] + "rules": { + "@typescript-eslint/no-unsafe-argument": "off", // 40 errors + "@typescript-eslint/no-unsafe-member-access": "off", // 167 errors + "@typescript-eslint/no-unsafe-assignment": "off", // 505 errors + "@typescript-eslint/consistent-type-assertions": "off", // 314 errors + "jest/no-restricted-matchers": "off", // 76 errors + "jsx-a11y/click-events-have-key-events": "off", // 10 errors + "jsx-a11y/anchor-is-valid": "off", // 27 errors + "jsx-a11y/no-static-element-interactions": "off", // 11 errors + "jsx-a11y/alt-text": "off", // 3 errors + "jsx-a11y/aria-props": "off", // 1 error + "jsx-a11y/no-noninteractive-tabindex": "off", // 1 error + "@typescript-eslint/no-unnecessary-condition": "off", // 79 errors + "@typescript-eslint/no-unsafe-call": "off", //66 errors + "@typescript-eslint/no-unsafe-return": "off", // 23 errors + "react-hooks/exhaustive-deps": "off", // 18 errors + "@typescript-eslint/restrict-template-expressions": "off", // 16 errors + "@typescript-eslint/await-thenable": "off", // 16 errors + "@bigcommerce/jsx-short-circuit-conditionals": "off", // 39 errors + "@typescript-eslint/no-misused-promises": "off", // 13 errors + "@typescript-eslint/no-floating-promises": "off", // 84 errors + "@typescript-eslint/naming-convention": "off", // 146 errors + "import/no-named-default": "off", // 21 errors + "no-underscore-dangle": "off", // 8 errors + "@typescript-eslint/no-use-before-define": "off", // 40 errors + "import/no-extraneous-dependencies": "off", // 4 errors + "@typescript-eslint/no-shadow": "off", // 3 errors + "react/jsx-no-useless-fragment": "off", // 7 errors + "jest/no-conditional-expect": "off", // 6 errors + "jest/no-identical-title": "off", // 8 errors + "no-plusplus": "off", // 3 errors + "no-nested-ternary": "off", // 5 errors + "no-restricted-globals": "off", // 3 errors + "@typescript-eslint/member-ordering": "off", // 3 errors + "react/jsx-no-bind": "off", // 5 errors + "complexity": "off", // 3 errors + "jest/no-if": "off", // 1 error + "array-callback-return": "off", // 2 errors + "@typescript-eslint/ban-ts-comment": "off", // 5 errors + "@typescript-eslint/no-explicit-any": "off", // 98 errors + "jest/no-standalone-expect": "off", // 1 error + "no-param-reassign": "off", // 2 errors + "jest/prefer-hooks-on-top": "off", // 1 error + "react/destructuring-assignment": "off", // 1 error + "@typescript-eslint/default-param-last": "off", // 1 error + "@typescript-eslint/require-await": "off", // 1 error + "no-template-curly-in-string": "off", // 1 error + "global-require": "off", // 22 errors + "@typescript-eslint/no-var-requires": "off", // 22 errors + "prettier/prettier": "off", // 11 errors + "jest/valid-expect": "off", // 19 errors + "no-multi-str": "off", // 1 error + "no-empty": "off", // 1 error + "no-proto": "off", // 1 error + "no-continue": "off", // 1 error + "no-new": "off", // 8 errors + "@typescript-eslint/no-empty-function": "off", // 1 error + "no-multi-assign": "off", // 1 error + "import/no-unresolved": "off" // 1 error + } } diff --git a/packages/core/src/app/address/AddressForm.spec.tsx b/packages/core/src/app/address/AddressForm.spec.tsx index a874c6df58..6480f11a51 100644 --- a/packages/core/src/app/address/AddressForm.spec.tsx +++ b/packages/core/src/app/address/AddressForm.spec.tsx @@ -1,4 +1,4 @@ -import { createCheckoutService, CheckoutService, FormField } from '@bigcommerce/checkout-sdk'; +import { CheckoutService, createCheckoutService, FormField } from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import { Formik } from 'formik'; import { noop } from 'lodash'; @@ -9,8 +9,8 @@ import { getStoreConfig } from '../config/config.mock'; import { createLocaleContext, LocaleContext, LocaleContextType } from '../locale'; import { DynamicFormField } from '../ui/form'; -import { getFormFields } from './formField.mock'; import AddressForm from './AddressForm'; +import { getFormFields } from './formField.mock'; describe('AddressForm Component', () => { let checkoutService: CheckoutService; @@ -29,107 +29,90 @@ describe('AddressForm Component', () => { it('renders all fields based on formFields', () => { component = mount( - - - + + + - + , ); - expect(component.find('[name="address.shouldSaveAddress"]').exists()) - .toEqual(false); + expect(component.find('[name="address.shouldSaveAddress"]').exists()).toBe(false); - expect(component.find(DynamicFormField).length).toEqual(formFields.length); + expect(component.find(DynamicFormField)).toHaveLength(formFields.length); }); it('renders DynamicFormField with expected props', () => { component = mount( - - + + - + , ); expect(component.find(DynamicFormField).at(0).props()).toEqual( expect.objectContaining({ parentFieldName: 'address', placeholder: undefined, - }) + }), ); - expect(component.find('[name="address.shouldSaveAddress"]').exists()) - .toEqual(true); + expect(component.find('[name="address.shouldSaveAddress"]').exists()).toBe(true); expect(component.find(DynamicFormField).at(0).prop('field')).toEqual( expect.objectContaining({ id: 'field_14', - }) + }), ); - expect(component.find(DynamicFormField).at(2).props().placeholder).toEqual('NO PO BOX'); + expect(component.find(DynamicFormField).at(2).props().placeholder).toBe('NO PO BOX'); }); it('renders calls onChange when a field is updated', () => { const onChange = jest.fn(); component = mount( - + - + - + , ); - component.find('input[name="address1"]') - .simulate('change', { target: { value: 'foo bar', name: 'address1' } }); + component + .find('input[name="address1"]') + .simulate('change', { target: { value: 'foo bar', name: 'address1' } }); expect(onChange).toHaveBeenCalledWith('address1', 'foo bar'); }); it('renders the same dropdown menu with different field.default values', () => { const field = formFields.find(({ name }) => name === 'field_27') as FormFieldType; + component = mount( - - - + + + - + , ); - const fieldChanged = {...field, default: 'new value'} as FormFieldType; + + const fieldChanged = { ...field, default: 'new value' } as FormFieldType; const componentChanged = mount( - - - + + + - + , ); expect(component.html()).toEqual(componentChanged.html()); diff --git a/packages/core/src/app/address/AddressForm.tsx b/packages/core/src/app/address/AddressForm.tsx index 4f3d4eabdd..10e18db5b5 100644 --- a/packages/core/src/app/address/AddressForm.tsx +++ b/packages/core/src/app/address/AddressForm.tsx @@ -1,15 +1,18 @@ import { Address, Country, FormField } from '@bigcommerce/checkout-sdk'; import { memoize } from '@bigcommerce/memoize'; import { forIn, noop } from 'lodash'; -import React, { createRef, Component, ReactNode, RefObject } from 'react'; +import React, { Component, createRef, ReactNode, RefObject } from 'react'; -import { withLanguage, TranslatedString, WithLanguageProps } from '../locale'; +import { TranslatedString, withLanguage, WithLanguageProps } from '../locale'; import { AutocompleteItem } from '../ui/autocomplete'; import { CheckboxFormField, DynamicFormField, DynamicFormFieldType, Fieldset } from '../ui/form'; import { AddressKeyMap } from './address'; -import { getAddressFormFieldInputId, getAddressFormFieldLegacyName } from './getAddressFormFieldInputId'; -import { mapToAddress, GoogleAutocompleteFormField } from './googleAutocomplete'; +import { + getAddressFormFieldInputId, + getAddressFormFieldLegacyName, +} from './getAddressFormFieldInputId'; +import { GoogleAutocompleteFormField, mapToAddress } from './googleAutocomplete'; import './AddressForm.scss'; export interface AddressFormProps { @@ -66,9 +69,10 @@ class AddressForm extends Component { private containerRef: RefObject = createRef(); private nextElement?: HTMLElement | null; - private handleDynamicFormFieldChange: (name: string) => (value: string | string[]) => void = memoize(name => value => { - this.syncNonFormikValue(name, value); - }); + private handleDynamicFormFieldChange: (name: string) => (value: string | string[]) => void = + memoize((name) => (value) => { + this.syncNonFormikValue(name, value); + }); componentDidMount(): void { const { current } = this.containerRef; @@ -89,55 +93,80 @@ class AddressForm extends Component { shouldShowSaveAddress, } = this.props; - return (<> -
-
}> - { formFields.map(field => { - const addressFieldName = field.name; - const translatedPlaceholderId = PLACEHOLDER[addressFieldName]; + return ( + <> +
+
} + > + {formFields.map((field) => { + const addressFieldName = field.name; + const translatedPlaceholderId = PLACEHOLDER[addressFieldName]; + + if ( + addressFieldName === 'address1' && + googleMapsApiKey && + countriesWithAutocomplete + ) { + return ( + + ); + } - if (addressFieldName === 'address1' && googleMapsApiKey && countriesWithAutocomplete) { return ( - + ) + } + onChange={this.handleDynamicFormFieldChange(addressFieldName)} + parentFieldName={ + field.custom + ? fieldName + ? `${fieldName}.customFields` + : 'customFields' + : fieldName + } + placeholder={this.getPlaceholderValue( + field, + translatedPlaceholderId, + )} /> ); - } - - return ( - } - onChange={ this.handleDynamicFormFieldChange(addressFieldName) } - parentFieldName={ field.custom ? - (fieldName ? `${fieldName}.customFields` : 'customFields') : - fieldName } - placeholder={ this.getPlaceholderValue(field, translatedPlaceholderId) } - /> - ); - }) } -
-
- { shouldShowSaveAddress && - } - name={ fieldName ? `${fieldName}.shouldSaveAddress` : 'shouldSaveAddress' } - /> } - ); + })} +
+
+ {shouldShowSaveAddress && ( + } + name={fieldName ? `${fieldName}.shouldSaveAddress` : 'shouldSaveAddress'} + /> + )} + + ); } private getPlaceholderValue(field: FormField, translatedPlaceholderId: string): string { @@ -145,12 +174,15 @@ class AddressForm extends Component { if (field.default && field.fieldType !== 'dropdown') { return field.default; - } else { - return translatedPlaceholderId && language.translate(translatedPlaceholderId); } + + return translatedPlaceholderId && language.translate(translatedPlaceholderId); } - private handleAutocompleteChange: (value: string, isOpen: boolean) => void = (value, isOpen) => { + private handleAutocompleteChange: (value: string, isOpen: boolean) => void = ( + value, + isOpen, + ) => { if (!isOpen) { this.syncNonFormikValue(AUTOCOMPLETE_FIELD_NAME, value); } @@ -158,13 +190,9 @@ class AddressForm extends Component { private handleAutocompleteSelect: ( place: google.maps.places.PlaceResult, - item: AutocompleteItem + item: AutocompleteItem, ) => void = (place, { value: autocompleteValue }) => { - const { - countries, - setFieldValue = noop, - onChange = noop, - } = this.props; + const { countries, setFieldValue = noop, onChange = noop } = this.props; const address = mapToAddress(place, countries); @@ -180,19 +208,15 @@ class AddressForm extends Component { // because autocomplete state is controlled by Downshift, we need to manually keep formik // value in sync when autocomplete value changes - private syncNonFormikValue: ( - fieldName: string, - value: string | string[] - ) => void = (fieldName, value) => { - const { - formFields, - setFieldValue = noop, - onChange = noop, - } = this.props; + private syncNonFormikValue: (fieldName: string, value: string | string[]) => void = ( + fieldName, + value, + ) => { + const { formFields, setFieldValue = noop, onChange = noop } = this.props; const dateFormFieldNames = formFields - .filter(field => field.custom && field.fieldType === DynamicFormFieldType.date) - .map(field => field.name); + .filter((field) => field.custom && field.fieldType === DynamicFormFieldType.date) + .map((field) => field.name); if (fieldName === AUTOCOMPLETE_FIELD_NAME || dateFormFieldNames.indexOf(fieldName) > -1) { setFieldValue(fieldName, value); diff --git a/packages/core/src/app/address/AddressFormModal.spec.tsx b/packages/core/src/app/address/AddressFormModal.spec.tsx index 036093f990..0f4b12d56d 100644 --- a/packages/core/src/app/address/AddressFormModal.spec.tsx +++ b/packages/core/src/app/address/AddressFormModal.spec.tsx @@ -1,4 +1,4 @@ -import { createCheckoutService, CheckoutService } from '@bigcommerce/checkout-sdk'; +import { CheckoutService, createCheckoutService } from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import React, { FunctionComponent } from 'react'; @@ -8,9 +8,9 @@ import { getStoreConfig } from '../config/config.mock'; import { createLocaleContext, LocaleContext, LocaleContextType } from '../locale'; import { Modal } from '../ui/modal'; -import { getFormFields } from './formField.mock'; import AddressForm from './AddressForm'; import AddressFormModal, { AddressFormModalProps } from './AddressFormModal'; +import { getFormFields } from './formField.mock'; describe('AddressFormModal Component', () => { let checkoutService: CheckoutService; @@ -35,13 +35,10 @@ describe('AddressFormModal Component', () => { countries: [], }; - TestComponent = props => ( - - - + TestComponent = (props) => ( + + + ); @@ -50,64 +47,69 @@ describe('AddressFormModal Component', () => { it('renders modal', () => { component = mount(); - expect(component.find(Modal).props()).toEqual(expect.objectContaining({ - isOpen: true, - shouldShowCloseButton: true, - })); + expect(component.find(Modal).props()).toEqual( + expect.objectContaining({ + isOpen: true, + shouldShowCloseButton: true, + }), + ); }); it('renders address form', () => { component = mount(); - expect(component.find(AddressForm).props()).toEqual(expect.objectContaining({ - countries: defaultProps.countries, - formFields: getFormFields(), - shouldShowSaveAddress: false, - })); + expect(component.find(AddressForm).props()).toEqual( + expect.objectContaining({ + countries: defaultProps.countries, + formFields: getFormFields(), + shouldShowSaveAddress: false, + }), + ); }); it('validates form on submission', async () => { component = mount(); - component.find('form') - .simulate('submit'); + component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(defaultProps.onSaveAddress) - .not.toHaveBeenCalled(); - - expect(component.find('[data-test="first-name-field-error-message"]').text()) - .toEqual('First Name is required'); + expect(defaultProps.onSaveAddress).not.toHaveBeenCalled(); + expect(component.find('[data-test="first-name-field-error-message"]').text()).toBe( + 'First Name is required', + ); }); it('submits form when valid', async () => { component = mount(); - component.find('input[name="firstName"]') + component + .find('input[name="firstName"]') .simulate('change', { target: { value: 'test', name: 'firstName' } }); - component.find('input[name="lastName"]') + component + .find('input[name="lastName"]') .simulate('change', { target: { value: 'foo', name: 'lastName' } }); - component.find('input[name="address1"]') + component + .find('input[name="address1"]') .simulate('change', { target: { value: 'l1', name: 'address1' } }); - component.find('form') - .simulate('submit'); + component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(defaultProps.onSaveAddress) - .toHaveBeenCalledWith(expect.objectContaining({ + expect(defaultProps.onSaveAddress).toHaveBeenCalledWith( + expect.objectContaining({ firstName: 'test', lastName: 'foo', address1: 'l1', - })); + }), + ); }); }); diff --git a/packages/core/src/app/address/AddressFormModal.tsx b/packages/core/src/app/address/AddressFormModal.tsx index 03847da295..19252eb860 100644 --- a/packages/core/src/app/address/AddressFormModal.tsx +++ b/packages/core/src/app/address/AddressFormModal.tsx @@ -1,18 +1,18 @@ import { Country, FormField } from '@bigcommerce/checkout-sdk'; -import { withFormik, FormikProps } from 'formik'; +import { FormikProps, withFormik } from 'formik'; import React, { FunctionComponent } from 'react'; import { lazy } from 'yup'; import { preventDefault } from '../common/dom'; -import { withLanguage, TranslatedString, WithLanguageProps } from '../locale'; +import { TranslatedString, withLanguage, WithLanguageProps } from '../locale'; import { Button, ButtonVariant } from '../ui/button'; import { Form } from '../ui/form'; import { LoadingOverlay } from '../ui/loading'; import { Modal, ModalHeader } from '../ui/modal'; +import AddressForm from './AddressForm'; import getAddressFormFieldsValidationSchema from './getAddressFormFieldsValidationSchema'; import { AddressFormValues } from './mapAddressToFormValues'; -import AddressForm from './AddressForm'; export interface AddressFormModalProps extends AddressFormProps { isOpen: boolean; @@ -31,7 +31,9 @@ export interface AddressFormProps { onRequestClose?(): void; } -const SaveAddress: FunctionComponent> = ({ +const SaveAddress: FunctionComponent< + AddressFormProps & WithLanguageProps & FormikProps +> = ({ googleMapsApiKey, getFields, countriesWithAutocomplete, @@ -42,30 +44,30 @@ const SaveAddress: FunctionComponent (
- +
@@ -74,36 +76,36 @@ const SaveAddress: FunctionComponent ); -const SaveAddressForm = withLanguage(withFormik({ - handleSubmit: (values, { props: { onSaveAddress } }) => { - onSaveAddress(values); - }, - mapPropsToValues: ({ defaultCountryCode = '' }) => ({ - firstName: '', - lastName: '', - address1: '', - address2: '', - customFields: {}, - country: '', - countryCode: defaultCountryCode, - stateOrProvince: '', - stateOrProvinceCode: '', - postalCode: '', - phone: '', - city: '', - company: '', - shouldSaveAddress: false, - }), - validationSchema: ({ - language, - getFields, - }: AddressFormProps & WithLanguageProps) => ( - lazy>(values => getAddressFormFieldsValidationSchema({ - language, - formFields: getFields(values && values.countryCode), - })) - ), -})(SaveAddress)); +const SaveAddressForm = withLanguage( + withFormik({ + handleSubmit: (values, { props: { onSaveAddress } }) => { + onSaveAddress(values); + }, + mapPropsToValues: ({ defaultCountryCode = '' }) => ({ + firstName: '', + lastName: '', + address1: '', + address2: '', + customFields: {}, + country: '', + countryCode: defaultCountryCode, + stateOrProvince: '', + stateOrProvinceCode: '', + postalCode: '', + phone: '', + city: '', + company: '', + shouldSaveAddress: false, + }), + validationSchema: ({ language, getFields }: AddressFormProps & WithLanguageProps) => + lazy>((values) => + getAddressFormFieldsValidationSchema({ + language, + formFields: getFields(values && values.countryCode), + }), + ), + })(SaveAddress), +); const AddressFormModal: FunctionComponent = ({ isOpen, @@ -118,15 +120,12 @@ const AddressFormModal: FunctionComponent = ({ } - isOpen={ isOpen } - onAfterOpen={ onAfterOpen } - onRequestClose={ onRequestClose } - shouldShowCloseButton={ true } + isOpen={isOpen} + onAfterOpen={onAfterOpen} + onRequestClose={onRequestClose} + shouldShowCloseButton={true} > - + ); diff --git a/packages/core/src/app/address/AddressSelect.spec.tsx b/packages/core/src/app/address/AddressSelect.spec.tsx index d87f603e20..032536b3a8 100644 --- a/packages/core/src/app/address/AddressSelect.spec.tsx +++ b/packages/core/src/app/address/AddressSelect.spec.tsx @@ -1,4 +1,4 @@ -import { createCheckoutService, CheckoutService } from '@bigcommerce/checkout-sdk'; +import { CheckoutService, createCheckoutService } from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import { noop } from 'lodash'; import React from 'react'; @@ -26,34 +26,34 @@ describe('AddressSelect Component', () => { jest.spyOn(checkoutService.getState().data, 'getConfig').mockReturnValue(getStoreConfig()); }); - it('renders `Enter Address` when there is no selected address ', () => { + it('renders `Enter Address` when there is no selected address', () => { component = mount( - - + + - + , ); - expect(component.find('#addressToggle').text()).toEqual('Enter a new address'); + expect(component.find('#addressToggle').text()).toBe('Enter a new address'); }); it('renders static address when there is a selected address', () => { component = mount( - - + + - + , ); expect(component.find(StaticAddress).prop('address')).toEqual(getAddress()); @@ -61,22 +61,23 @@ describe('AddressSelect Component', () => { it('renders addresses menu when select component is clicked', () => { component = mount( - - + + - + , ); component.find('#addressToggle').simulate('click'); + const options = component.find('#addressDropdown li'); - expect(options.first().text()).toEqual('Enter a new address'); + expect(options.first().text()).toBe('Enter a new address'); expect(options.find(StaticAddress).prop('address')).toEqual(getCustomer().addresses[0]); }); @@ -85,15 +86,15 @@ describe('AddressSelect Component', () => { const onUseNewAddress = jest.fn(); component = mount( - - + + - + , ); component.find('#addressToggle').simulate('click'); @@ -110,16 +111,16 @@ describe('AddressSelect Component', () => { const onSelectAddress = jest.fn(); component = mount( - - + + - + , ); component.find('#addressToggle').simulate('click'); diff --git a/packages/core/src/app/address/AddressSelect.tsx b/packages/core/src/app/address/AddressSelect.tsx index 345ecc0c9a..cb85438107 100644 --- a/packages/core/src/app/address/AddressSelect.tsx +++ b/packages/core/src/app/address/AddressSelect.tsx @@ -1,5 +1,5 @@ import { Address, CustomerAddress } from '@bigcommerce/checkout-sdk'; -import React, { memo, FunctionComponent, PureComponent, ReactNode } from 'react'; +import React, { FunctionComponent, memo, PureComponent, ReactNode } from 'react'; import { preventDefault } from '../common/dom'; import { TranslatedString } from '../locale'; @@ -19,10 +19,7 @@ export interface AddressSelectProps { class AddressSelect extends PureComponent { render(): ReactNode { - const { - addresses, - selectedAddress, - } = this.props; + const { addresses, selectedAddress } = this.props; return (
@@ -30,16 +27,16 @@ class AddressSelect extends PureComponent { } >
@@ -48,10 +45,7 @@ class AddressSelect extends PureComponent { } private handleSelectAddress: (newAddress: Address) => void = (newAddress: Address) => { - const { - onSelectAddress, - selectedAddress, - } = this.props; + const { onSelectAddress, selectedAddress } = this.props; if (!isEqualAddress(selectedAddress, newAddress)) { onSelectAddress(newAddress); @@ -59,10 +53,7 @@ class AddressSelect extends PureComponent { }; private handleUseNewAddress: () => void = () => { - const { - selectedAddress, - onUseNewAddress, - } = this.props; + const { selectedAddress, onUseNewAddress } = this.props; onUseNewAddress(selectedAddress); }; @@ -74,29 +65,23 @@ const AddressSelectMenu: FunctionComponent = ({ onUseNewAddress, selectedAddress, }) => ( -
    + ); diff --git a/packages/core/src/app/address/AddressSelectButton.tsx b/packages/core/src/app/address/AddressSelectButton.tsx index 137fa815d9..bc8859665a 100644 --- a/packages/core/src/app/address/AddressSelectButton.tsx +++ b/packages/core/src/app/address/AddressSelectButton.tsx @@ -1,7 +1,7 @@ import React, { FunctionComponent, useState } from 'react'; import { preventDefault } from '../common/dom'; -import { withLanguage, TranslatedString, WithLanguageProps } from '../locale'; +import { TranslatedString, withLanguage, WithLanguageProps } from '../locale'; import { AddressSelectProps } from './AddressSelect'; import StaticAddress from './StaticAddress'; @@ -15,19 +15,22 @@ const AddressSelectButton: FunctionComponent setAriaExpanded(!ariaExpanded)) } - > - { selectedAddress ? - : - } - -)}; + setAriaExpanded(!ariaExpanded))} + > + {selectedAddress ? ( + + ) : ( + + )} + + ); +}; export default withLanguage(AddressSelectButton); diff --git a/packages/core/src/app/address/StaticAddress.spec.tsx b/packages/core/src/app/address/StaticAddress.spec.tsx index 3cc827533b..a6892263a4 100644 --- a/packages/core/src/app/address/StaticAddress.spec.tsx +++ b/packages/core/src/app/address/StaticAddress.spec.tsx @@ -1,4 +1,8 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, +} from '@bigcommerce/checkout-sdk'; import { mount, render } from 'enzyme'; import React, { FunctionComponent } from 'react'; @@ -6,8 +10,8 @@ import { CheckoutProvider } from '../checkout'; import { getCountries } from '../geography/countries.mock'; import { getAddress } from './address.mock'; -import { getAddressFormFields } from './formField.mock'; import AddressType from './AddressType'; +import { getAddressFormFields } from './formField.mock'; import StaticAddress, { StaticAddressProps } from './StaticAddress'; describe('StaticAddress Component', () => { @@ -24,129 +28,122 @@ describe('StaticAddress Component', () => { address: getAddress(), }; - jest.spyOn(checkoutState.data, 'getBillingCountries') - .mockReturnValue(getCountries()); + jest.spyOn(checkoutState.data, 'getBillingCountries').mockReturnValue(getCountries()); - jest.spyOn(checkoutState.data, 'getBillingAddressFields') - .mockReturnValue(getAddressFormFields()); + jest.spyOn(checkoutState.data, 'getBillingAddressFields').mockReturnValue( + getAddressFormFields(), + ); - jest.spyOn(checkoutState.data, 'getShippingAddressFields') - .mockReturnValue(getAddressFormFields()); + jest.spyOn(checkoutState.data, 'getShippingAddressFields').mockReturnValue( + getAddressFormFields(), + ); - StaticAddressTest = props => ( - - + StaticAddressTest = (props) => ( + + ); }); it('renders component with supplied props', () => { - const tree = render(); + const tree = render(); expect(tree).toMatchSnapshot(); }); it('renders component when props are missing', () => { - const tree = render(); + const tree = render( + , + ); expect(tree).toMatchSnapshot(); }); it('renders component if required fields for billing address are not missing', () => { - const container = mount(); + const container = mount(); - expect(checkoutState.data.getBillingAddressFields) - .toHaveBeenCalled(); + expect(checkoutState.data.getBillingAddressFields).toHaveBeenCalled(); - expect(container.find('.address-line-1').text()) - .toContain(defaultProps.address.address1); + expect(container.find('.address-line-1').text()).toContain(defaultProps.address.address1); }); it('does not render component if required fields for billing address are missing', () => { - const container = mount(); - - expect(checkoutState.data.getBillingAddressFields) - .toHaveBeenCalled(); - - expect(container.html()) - .toEqual(''); + const container = mount( + , + ); + + expect(checkoutState.data.getBillingAddressFields).toHaveBeenCalled(); + + expect(container.html()).toBe(''); }); it('renders component if required fields for shipping address are not missing', () => { - const container = mount(); + const container = mount( + , + ); - expect(checkoutState.data.getShippingAddressFields) - .toHaveBeenCalled(); + expect(checkoutState.data.getShippingAddressFields).toHaveBeenCalled(); - expect(container.find('.address-line-1').text()) - .toContain(defaultProps.address.address1); + expect(container.find('.address-line-1').text()).toContain(defaultProps.address.address1); }); it('does not render component if required fields for shipping address are missing', () => { - const container = mount(); - - expect(checkoutState.data.getShippingAddressFields) - .toHaveBeenCalled(); - - expect(container.html()) - .toEqual(''); + const container = mount( + , + ); + + expect(checkoutState.data.getShippingAddressFields).toHaveBeenCalled(); + + expect(container.html()).toBe(''); }); it('renders component if only custom fields are missing', () => { - jest.spyOn(checkoutState.data, 'getBillingAddressFields') - .mockReturnValue([ - ...getAddressFormFields(), - { - custom: true, - default: undefined, - fieldType: 'text', - id: 'foobar', - label: 'Custom number', - name: 'foobar', - required: true, - type: 'integer', - }, - ]); - - const container = mount(); - - expect(container.html()) - .not.toEqual(''); + jest.spyOn(checkoutState.data, 'getBillingAddressFields').mockReturnValue([ + ...getAddressFormFields(), + { + custom: true, + default: undefined, + fieldType: 'text', + id: 'foobar', + label: 'Custom number', + name: 'foobar', + required: true, + type: 'integer', + }, + ]); + + const container = mount( + , + ); + + expect(container.html()).not.toBe(''); }); }); diff --git a/packages/core/src/app/address/StaticAddress.tsx b/packages/core/src/app/address/StaticAddress.tsx index 8d8c6f0d8e..d5d89d0c1d 100644 --- a/packages/core/src/app/address/StaticAddress.tsx +++ b/packages/core/src/app/address/StaticAddress.tsx @@ -1,12 +1,18 @@ -import { Address, CheckoutSelectors, Country, FormField, ShippingInitializeOptions } from '@bigcommerce/checkout-sdk'; +import { + Address, + CheckoutSelectors, + Country, + FormField, + ShippingInitializeOptions, +} from '@bigcommerce/checkout-sdk'; import { isEmpty } from 'lodash'; -import React, { memo, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo } from 'react'; -import { withCheckout, CheckoutContextProps } from '../checkout'; +import { CheckoutContextProps, withCheckout } from '../checkout'; +import AddressType from './AddressType'; import isValidAddress from './isValidAddress'; import localizeAddress from './localizeAddress'; -import AddressType from './AddressType'; import './StaticAddress.scss'; export interface StaticAddressProps { @@ -23,88 +29,76 @@ interface WithCheckoutStaticAddressProps { fields?: FormField[]; } -const StaticAddress: FunctionComponent = ({ - countries, - fields, - address: addressWithoutLocalization, -}) => { +const StaticAddress: FunctionComponent< + StaticAddressEditableProps & WithCheckoutStaticAddressProps +> = ({ countries, fields, address: addressWithoutLocalization }) => { const address = localizeAddress(addressWithoutLocalization, countries); - const isValid = !fields ? !isEmpty(address) : isValidAddress( - address, - fields.filter(field => !field.custom) - ); + const isValid = !fields + ? !isEmpty(address) + : isValidAddress( + address, + fields.filter((field) => !field.custom), + ); - return !isValid ? null :
    - { - (address.firstName || address.lastName) && -

    - { `${address.firstName} ` } - { address.lastName } -

    - } + return !isValid ? null : ( +
    + {(address.firstName || address.lastName) && ( +

    + {`${address.firstName} `} + {address.lastName} +

    + )} - { - (address.phone || address.company) && -

    - { `${address.company} ` } - { address.phone } -

    - } + {(address.phone || address.company) && ( +

    + {`${address.company} `} + {address.phone} +

    + )} -
    -

    - { `${address.address1} ` } - { - address.address2 && - - { ` / ${address.address2 }` } - - } -

    +
    +

    + {`${address.address1} `} + {address.address2 && ( + {` / ${address.address2}`} + )} +

    -

    - { - address.city && - { `${address.city}, ` } - } - { - address.localizedProvince && - { `${address.localizedProvince}, ` } - } - { - address.postalCode && - { `${address.postalCode} / ` } - } - { - address.localizedCountry && - { `${address.localizedCountry} ` } - } -

    +

    + {address.city && {`${address.city}, `}} + {address.localizedProvince && ( + {`${address.localizedProvince}, `} + )} + {address.postalCode && ( + {`${address.postalCode} / `} + )} + {address.localizedCountry && ( + {`${address.localizedCountry} `} + )} +

    +
    -
    ; + ); }; export function mapToStaticAddressProps( context: CheckoutContextProps, - { address, type }: StaticAddressProps + { address, type }: StaticAddressProps, ): WithCheckoutStaticAddressProps | null { const { checkoutState: { - data: { - getBillingCountries, - getBillingAddressFields, - getShippingAddressFields, - }, + data: { getBillingCountries, getBillingAddressFields, getShippingAddressFields }, }, } = context; return { countries: getBillingCountries(), - fields: type === AddressType.Billing ? - getBillingAddressFields(address.countryCode) : - type === AddressType.Shipping ? - getShippingAddressFields(address.countryCode) : - undefined, + fields: + type === AddressType.Billing + ? getBillingAddressFields(address.countryCode) + : type === AddressType.Shipping + ? getShippingAddressFields(address.countryCode) + : undefined, }; } diff --git a/packages/core/src/app/address/getAddressFormFieldInputId.ts b/packages/core/src/app/address/getAddressFormFieldInputId.ts index 101e19f6bb..8f08e4ed16 100644 --- a/packages/core/src/app/address/getAddressFormFieldInputId.ts +++ b/packages/core/src/app/address/getAddressFormFieldInputId.ts @@ -15,6 +15,7 @@ export function getAddressFormFieldLegacyName(name: string): string { export function getAddressFormFieldInputId(name: string): string { return `${getAddressFormFieldLegacyName(name)}Input`; } + export function getAddressFormFieldLabelId(name: string): string { return `${getAddressFormFieldLegacyName(name)}Label`; } diff --git a/packages/core/src/app/address/getAddressFormFieldsValidationSchema.ts b/packages/core/src/app/address/getAddressFormFieldsValidationSchema.ts index 51fe16baef..c77ae7d5c0 100644 --- a/packages/core/src/app/address/getAddressFormFieldsValidationSchema.ts +++ b/packages/core/src/app/address/getAddressFormFieldsValidationSchema.ts @@ -2,14 +2,20 @@ import { FormField, LanguageService } from '@bigcommerce/checkout-sdk'; import { memoize } from 'lodash'; import { ObjectSchema } from 'yup'; -import { getFormFieldsValidationSchema, FormFieldValues, TranslateValidationErrorFunction } from '../formFields'; +import { + FormFieldValues, + getFormFieldsValidationSchema, + TranslateValidationErrorFunction, +} from '../formFields'; export interface AddressFormFieldsValidationSchemaOptions { formFields: FormField[]; language?: LanguageService; } -export function getTranslateAddressError(language?: LanguageService): TranslateValidationErrorFunction { +export function getTranslateAddressError( + language?: LanguageService, +): TranslateValidationErrorFunction { const requiredFieldErrorTranslationIds: { [fieldName: string]: string } = { countryCode: 'address.country', firstName: 'address.first_name', @@ -31,10 +37,12 @@ export function getTranslateAddressError(language?: LanguageService): TranslateV if (type === 'required') { if (requiredFieldErrorTranslationIds[name]) { - return language.translate(`${requiredFieldErrorTranslationIds[name]}_required_error`); - } else { - return language.translate(`address.custom_required_error`, { label }); + return language.translate( + `${requiredFieldErrorTranslationIds[name]}_required_error`, + ); } + + return language.translate(`address.custom_required_error`, { label }); } if (type === 'max' && max) { @@ -48,8 +56,6 @@ export function getTranslateAddressError(language?: LanguageService): TranslateV if (type === 'invalid') { return language.translate(`address.invalid_characters_error`, { label }); } - - return; }; } diff --git a/packages/core/src/app/address/googleAutocomplete/AddressSelector.spec.ts b/packages/core/src/app/address/googleAutocomplete/AddressSelector.spec.ts index a22431eaa7..7fdcd44c52 100644 --- a/packages/core/src/app/address/googleAutocomplete/AddressSelector.spec.ts +++ b/packages/core/src/app/address/googleAutocomplete/AddressSelector.spec.ts @@ -1,5 +1,5 @@ -import { getGoogleAutocompletePlaceMock } from './googleAutocompleteResult.mock'; import AddressSelector from './AddressSelector'; +import { getGoogleAutocompletePlaceMock } from './googleAutocompleteResult.mock'; describe('AddressSelector', () => { let googleAutoCompleteResponseMock: google.maps.places.PlaceResult; @@ -11,6 +11,7 @@ describe('AddressSelector', () => { describe('constructor', () => { it('returns an instance of generic country accessor', () => { const accessor = new AddressSelector(googleAutoCompleteResponseMock); + expect(accessor).toBeInstanceOf(AddressSelector); }); }); @@ -18,6 +19,7 @@ describe('AddressSelector', () => { describe('#getState()', () => { it('returns the correct state', () => { const accessor = new AddressSelector(googleAutoCompleteResponseMock); + expect(accessor.getState()).toBe('NSW'); }); }); @@ -25,6 +27,7 @@ describe('AddressSelector', () => { describe('#getStreet()', () => { it('returns the correct street', () => { const accessor = new AddressSelector(googleAutoCompleteResponseMock); + expect(accessor.getStreet()).toBe('1-3 Smail St'); }); }); @@ -32,6 +35,7 @@ describe('AddressSelector', () => { describe('#getStreet2()', () => { it('returns the correct street2 value', () => { const accessor = new AddressSelector(googleAutoCompleteResponseMock); + expect(accessor.getStreet2()).toBe('unit 6'); }); }); @@ -39,6 +43,7 @@ describe('AddressSelector', () => { describe('#getCountry()', () => { it('returns the correct country', () => { const accessor = new AddressSelector(googleAutoCompleteResponseMock); + expect(accessor.getCountry()).toBe('AU'); }); }); @@ -46,6 +51,7 @@ describe('AddressSelector', () => { describe('#getPostCode()', () => { it('returns the correct post code', () => { const accessor = new AddressSelector(googleAutoCompleteResponseMock); + expect(accessor.getPostCode()).toBe('2007'); }); }); @@ -53,13 +59,15 @@ describe('AddressSelector', () => { describe('#getCity()', () => { it('returns the postal town as city if present', () => { const accessor = new AddressSelector(googleAutoCompleteResponseMock); + expect(accessor.getCity()).toBe('Ultimo PT (l)'); }); it('returns the locality as city if no postal town', () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const addressComponents = googleAutoCompleteResponseMock.address_components! - .filter(address => !address.types.includes('postal_town')); + const addressComponents = googleAutoCompleteResponseMock.address_components!.filter( + (address) => !address.types.includes('postal_town'), + ); const accessor = new AddressSelector({ ...googleAutoCompleteResponseMock, @@ -71,8 +79,10 @@ describe('AddressSelector', () => { it('returns the neighborhood as city if nothing else is present', () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const addressComponents = googleAutoCompleteResponseMock.address_components! - .filter(address => !address.types.includes('postal_town') && !address.types.includes('locality')); + const addressComponents = googleAutoCompleteResponseMock.address_components!.filter( + (address) => + !address.types.includes('postal_town') && !address.types.includes('locality'), + ); const accessor = new AddressSelector({ ...googleAutoCompleteResponseMock, diff --git a/packages/core/src/app/address/googleAutocomplete/AddressSelector.ts b/packages/core/src/app/address/googleAutocomplete/AddressSelector.ts index be4a8fe647..76e5346590 100644 --- a/packages/core/src/app/address/googleAutocomplete/AddressSelector.ts +++ b/packages/core/src/app/address/googleAutocomplete/AddressSelector.ts @@ -4,9 +4,7 @@ export default class AddressSelector { protected _address: google.maps.GeocoderAddressComponent[] | undefined; protected _name: string; - constructor( - googlePlace: google.maps.places.PlaceResult - ) { + constructor(googlePlace: google.maps.places.PlaceResult) { const { address_components, name } = googlePlace; this._name = name; @@ -26,9 +24,11 @@ export default class AddressSelector { } getCity(): string { - return this._get('postal_town', 'long_name') || + return ( + this._get('postal_town', 'long_name') || this._get('locality', 'long_name') || - this._get('neighborhood', 'short_name'); + this._get('neighborhood', 'short_name') + ); } getCountry(): string { @@ -41,9 +41,10 @@ export default class AddressSelector { protected _get( type: GoogleAddressFieldType, - access: Exclude + access: Exclude, ): string { - const element = this._address && this._address.find(field => field.types.indexOf(type) !== -1); + const element = + this._address && this._address.find((field) => field.types.indexOf(type) !== -1); if (element) { return element[access]; diff --git a/packages/core/src/app/address/googleAutocomplete/AddressSelectorFactory.ts b/packages/core/src/app/address/googleAutocomplete/AddressSelectorFactory.ts index ec389e4b4c..ef143725bf 100644 --- a/packages/core/src/app/address/googleAutocomplete/AddressSelectorFactory.ts +++ b/packages/core/src/app/address/googleAutocomplete/AddressSelectorFactory.ts @@ -6,8 +6,8 @@ export default class AddressSelectorFactory { const addressSelector = new AddressSelector(autocompleteData); switch (addressSelector.getCountry()) { - case 'GB': - return new AddressSelectorUK(autocompleteData); + case 'GB': + return new AddressSelectorUK(autocompleteData); } return addressSelector; diff --git a/packages/core/src/app/address/googleAutocomplete/GoogleAutocomplete.spec.tsx b/packages/core/src/app/address/googleAutocomplete/GoogleAutocomplete.spec.tsx index 03d69524ab..a22d123ada 100644 --- a/packages/core/src/app/address/googleAutocomplete/GoogleAutocomplete.spec.tsx +++ b/packages/core/src/app/address/googleAutocomplete/GoogleAutocomplete.spec.tsx @@ -9,7 +9,13 @@ describe('GoogleAutocomplete Component', () => { // At the moment, jest.mock('...') doesn't seem to do the trick. it('renders input with initial value', () => { - const tree = render(); + const tree = render( + , + ); expect(toJson(tree)).toMatchSnapshot(); }); diff --git a/packages/core/src/app/address/googleAutocomplete/GoogleAutocomplete.tsx b/packages/core/src/app/address/googleAutocomplete/GoogleAutocomplete.tsx index 677e5776c5..319ead59b7 100644 --- a/packages/core/src/app/address/googleAutocomplete/GoogleAutocomplete.tsx +++ b/packages/core/src/app/address/googleAutocomplete/GoogleAutocomplete.tsx @@ -39,62 +39,51 @@ class GoogleAutocomplete extends PureComponent
    ); } - private onSelect: (item: AutocompleteItem) => void = item => { - const { - fields, - onSelect = noop, - nextElement, - } = this.props; - - this.googleAutocompleteService.getPlacesServices().then(service => { - service.getDetails({ - placeId: item.id, - fields: fields || ['address_components', 'name'], - }, result => { - if (nextElement) { - nextElement.focus(); - } - - onSelect(result, item); - }); + private onSelect: (item: AutocompleteItem) => void = (item) => { + const { fields, onSelect = noop, nextElement } = this.props; + + this.googleAutocompleteService.getPlacesServices().then((service) => { + service.getDetails( + { + placeId: item.id, + fields: fields || ['address_components', 'name'], + }, + (result) => { + if (nextElement) { + nextElement.focus(); + } + + onSelect(result, item); + }, + ); }); }; - private onChange: (input: string) => void = input => { - const { - isAutocompleteEnabled, - onChange = noop, - } = this.props; + private onChange: (input: string) => void = (input) => { + const { isAutocompleteEnabled, onChange = noop } = this.props; onChange(input, false); @@ -113,18 +102,16 @@ class GoogleAutocomplete extends PureComponent { - service.getPlacePredictions({ - input, - types: types || ['geocode'], - componentRestrictions, - }, results => - this.setState({ items: this.toAutocompleteItems(results) }) + const { componentRestrictions, types } = this.props; + + this.googleAutocompleteService.getAutocompleteService().then((service) => { + service.getPlacePredictions( + { + input, + types: types || ['geocode'], + componentRestrictions, + }, + (results) => this.setState({ items: this.toAutocompleteItems(results) }), ); }); } @@ -143,8 +130,10 @@ class GoogleAutocomplete extends PureComponent ({ + private toAutocompleteItems( + results?: google.maps.places.AutocompletePrediction[], + ): AutocompleteItem[] { + return (results || []).map((result) => ({ label: result.description, value: result.structured_formatting.main_text, highlightedSlices: result.matched_substrings, diff --git a/packages/core/src/app/address/googleAutocomplete/GoogleAutocompleteFormField.tsx b/packages/core/src/app/address/googleAutocomplete/GoogleAutocompleteFormField.tsx index f71b5cb4db..cb1f7208ae 100644 --- a/packages/core/src/app/address/googleAutocomplete/GoogleAutocompleteFormField.tsx +++ b/packages/core/src/app/address/googleAutocomplete/GoogleAutocompleteFormField.tsx @@ -1,11 +1,14 @@ import { FormField as FormFieldType } from '@bigcommerce/checkout-sdk'; import { FieldProps } from 'formik'; -import React, { memo, useCallback, useMemo, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo, useCallback, useMemo } from 'react'; import { TranslatedString } from '../../locale'; import { AutocompleteItem } from '../../ui/autocomplete'; import { FormField, Label } from '../../ui/form'; -import { getAddressFormFieldInputId, getAddressFormFieldLabelId } from '../getAddressFormFieldInputId'; +import { + getAddressFormFieldInputId, + getAddressFormFieldLabelId, +} from '../getAddressFormFieldInputId'; import GoogleAutocomplete from './GoogleAutocomplete'; @@ -21,11 +24,8 @@ export interface GoogleAutocompleteFormFieldProps { onChange(value: string, isOpen: boolean): void; } -const GoogleAutocompleteFormField: FunctionComponent = ({ - field: { - default: placeholder, - name, - }, +const GoogleAutocompleteFormField: FunctionComponent = ({ + field: { default: placeholder, name }, countryCode, supportedCountries, parentFieldName, @@ -37,52 +37,58 @@ const GoogleAutocompleteFormField: FunctionComponent { const fieldName = parentFieldName ? `${parentFieldName}.${name}` : name; - const labelContent = useMemo(() => ( - - ), []); + const labelContent = useMemo(() => , []); const labelId = getAddressFormFieldLabelId(name); - const inputProps = useMemo(() => ({ - className: 'form-input optimizedCheckout-form-input', - id: getAddressFormFieldInputId(name), - 'aria-labelledby': labelId, - placeholder, - }), [name, labelId, placeholder]); + const inputProps = useMemo( + () => ({ + className: 'form-input optimizedCheckout-form-input', + id: getAddressFormFieldInputId(name), + 'aria-labelledby': labelId, + placeholder, + }), + [name, labelId, placeholder], + ); - const renderInput = useCallback(({ field }: FieldProps) => ( - -1 : - false } - nextElement={ nextElement } - onChange={ onChange } - onSelect={ onSelect } - onToggleOpen={ onToggleOpen } - /> - ), [ - apiKey, - countryCode, - inputProps, - nextElement, - onChange, - onSelect, - onToggleOpen, - supportedCountries, - ]); + const renderInput = useCallback( + ({ field }: FieldProps) => ( + -1 : false + } + nextElement={nextElement} + onChange={onChange} + onSelect={onSelect} + onToggleOpen={onToggleOpen} + /> + ), + [ + apiKey, + countryCode, + inputProps, + nextElement, + onChange, + onSelect, + onToggleOpen, + supportedCountries, + ], + ); return ( -
    +
    { labelContent } } - name={ fieldName } + input={renderInput} + label={ + + } + name={fieldName} />
    ); diff --git a/packages/core/src/app/address/googleAutocomplete/GoogleAutocompleteScriptLoader.ts b/packages/core/src/app/address/googleAutocomplete/GoogleAutocompleteScriptLoader.ts index e3c16eb9e3..cb9ef9aeb2 100644 --- a/packages/core/src/app/address/googleAutocomplete/GoogleAutocompleteScriptLoader.ts +++ b/packages/core/src/app/address/googleAutocomplete/GoogleAutocompleteScriptLoader.ts @@ -32,8 +32,9 @@ export default class GoogleAutocompleteScriptLoader { reject(); }; - this._scriptLoader.loadScript(`//maps.googleapis.com/maps/api/js?${params}`) - .catch(e => { + this._scriptLoader + .loadScript(`//maps.googleapis.com/maps/api/js?${params}`) + .catch((e) => { this._googleAutoComplete = undefined; throw e; }); @@ -46,9 +47,11 @@ export default class GoogleAutocompleteScriptLoader { function isAutocompleteWindow(window: Window): window is GoogleAutocompleteWindow { const autocompleteWindow = window as GoogleAutocompleteWindow; - return Boolean(autocompleteWindow.google && - autocompleteWindow.google.maps && - autocompleteWindow.google.maps.places); + return Boolean( + autocompleteWindow.google && + autocompleteWindow.google.maps && + autocompleteWindow.google.maps.places, + ); } export interface GoogleCallbackWindow extends Window { diff --git a/packages/core/src/app/address/googleAutocomplete/GoogleAutocompleteService.ts b/packages/core/src/app/address/googleAutocomplete/GoogleAutocompleteService.ts index 14f3806a5a..d8de9a2afb 100644 --- a/packages/core/src/app/address/googleAutocomplete/GoogleAutocompleteService.ts +++ b/packages/core/src/app/address/googleAutocomplete/GoogleAutocompleteService.ts @@ -7,13 +7,14 @@ export default class GoogleAutocompleteService { constructor( private _apiKey: string, - private _scriptLoader: GoogleAutocompleteScriptLoader = getGoogleAutocompleteScriptLoader() + private _scriptLoader: GoogleAutocompleteScriptLoader = getGoogleAutocompleteScriptLoader(), ) {} getAutocompleteService(): Promise { if (!this._autocompletePromise) { - this._autocompletePromise = this._scriptLoader.loadMapsSdk(this._apiKey) - .then(googleMapsSdk => { + this._autocompletePromise = this._scriptLoader + .loadMapsSdk(this._apiKey) + .then((googleMapsSdk) => { if (!googleMapsSdk.places.AutocompleteService) { throw new Error('`AutocompleteService` is undefined'); } @@ -29,8 +30,9 @@ export default class GoogleAutocompleteService { const node = document.createElement('div'); if (!this._placesPromise) { - this._placesPromise = this._scriptLoader.loadMapsSdk(this._apiKey) - .then(googleMapsSdk => { + this._placesPromise = this._scriptLoader + .loadMapsSdk(this._apiKey) + .then((googleMapsSdk) => { if (!googleMapsSdk.places.PlacesService) { throw new Error('`PlacesService` is undefined'); } diff --git a/packages/core/src/app/address/googleAutocomplete/getGoogleAutocompleteScriptLoader.spec.ts b/packages/core/src/app/address/googleAutocomplete/getGoogleAutocompleteScriptLoader.spec.ts index d34933d3a5..21fb0655fa 100644 --- a/packages/core/src/app/address/googleAutocomplete/getGoogleAutocompleteScriptLoader.spec.ts +++ b/packages/core/src/app/address/googleAutocomplete/getGoogleAutocompleteScriptLoader.spec.ts @@ -1,7 +1,9 @@ import { getScriptLoader, ScriptLoader } from '@bigcommerce/script-loader'; +import GoogleAutocompleteScriptLoader, { + GoogleCallbackWindow, +} from './GoogleAutocompleteScriptLoader'; import { GoogleAutocompleteWindow } from './googleAutocompleteTypes'; -import GoogleAutocompleteScriptLoader, { GoogleCallbackWindow } from './GoogleAutocompleteScriptLoader'; describe('GoogleAutocompleteScriptLoader', () => { const scriptLoader: ScriptLoader = getScriptLoader(); @@ -33,13 +35,14 @@ describe('GoogleAutocompleteScriptLoader', () => { }); it('calls loadScript with the right parameters', () => { - expect(scriptLoader.loadScript) - .toHaveBeenCalledWith([ + expect(scriptLoader.loadScript).toHaveBeenCalledWith( + [ '//maps.googleapis.com/maps/api/js?language=en', 'key=foo', 'libraries=places', 'callback=initAutoComplete', - ].join('&')); + ].join('&'), + ); }); it('calls loadScript once', async () => { @@ -47,8 +50,7 @@ describe('GoogleAutocompleteScriptLoader', () => { await googleScriptLoader.loadMapsSdk('y'); await googleScriptLoader.loadMapsSdk('z'); - expect(scriptLoader.loadScript) - .toHaveBeenCalledTimes(1); + expect(scriptLoader.loadScript).toHaveBeenCalledTimes(1); }); }); }); diff --git a/packages/core/src/app/address/googleAutocomplete/googleAutocompleteResult.mock.ts b/packages/core/src/app/address/googleAutocomplete/googleAutocompleteResult.mock.ts index 8e3e2bd219..b02135b622 100644 --- a/packages/core/src/app/address/googleAutocomplete/googleAutocompleteResult.mock.ts +++ b/packages/core/src/app/address/googleAutocomplete/googleAutocompleteResult.mock.ts @@ -5,76 +5,52 @@ export function getGoogleAutocompletePlaceMock(): google.maps.places.PlaceResult { long_name: 'unit 6', short_name: 'unit 6', - types: [ - 'subpremise', - ], + types: ['subpremise'], }, { long_name: '1-3 (l)', short_name: '1-3 (s)', - types: [ - 'street_number', - ], + types: ['street_number'], }, { long_name: 'Smail Street', short_name: 'Smail St', - types: [ - 'route', - ], + types: ['route'], }, { long_name: 'Ultimo PT (l)', short_name: 'Ultimo PT', - types: [ - 'postal_town', - ], + types: ['postal_town'], }, { long_name: 'Ultimo (l)', short_name: 'Ultimo', - types: [ - 'locality', - 'political', - ], + types: ['locality', 'political'], }, { long_name: 'Ultimo N (l)', short_name: 'Ultimo N', - types: [ - 'neighborhood', - ], + types: ['neighborhood'], }, { long_name: 'Council of the City of Sydney', short_name: 'Sydney', - types: [ - 'administrative_area_level_2', - 'political', - ], + types: ['administrative_area_level_2', 'political'], }, { long_name: 'New South Wales', short_name: 'NSW', - types: [ - 'administrative_area_level_1', - 'political', - ], + types: ['administrative_area_level_1', 'political'], }, { long_name: 'Australia', short_name: 'AU', - types: [ - 'country', - 'political', - ], + types: ['country', 'political'], }, { long_name: '2007 (l)', short_name: '2007', - types: [ - 'postal_code', - ], + types: ['postal_code'], }, ], } as google.maps.places.PlaceResult; diff --git a/packages/core/src/app/address/googleAutocomplete/googleAutocompleteTypes.ts b/packages/core/src/app/address/googleAutocomplete/googleAutocompleteTypes.ts index 77f66668f5..3b408eff69 100644 --- a/packages/core/src/app/address/googleAutocomplete/googleAutocompleteTypes.ts +++ b/packages/core/src/app/address/googleAutocomplete/googleAutocompleteTypes.ts @@ -1,28 +1,28 @@ -export type GoogleAutocompleteOptionTypes = 'establishment' | 'geocode' | 'address'; +export type GoogleAutocompleteOptionTypes = 'establishment' | 'geocode' | 'address'; export type GoogleAutocompleteFields = - 'address_components' | - 'adr_address' | - 'aspects' | - 'formatted_address' | - 'formatted_phone_number' | - 'geometry' | - 'html_attributions' | - 'icon' | - 'international_phone_number' | - 'name' | - 'opening_hours' | - 'photos' | - 'place_id' | - 'plus_code' | - 'price_level' | - 'rating' | - 'reviews' | - 'types' | - 'url' | - 'utc_offset' | - 'vicinity' | - 'website'; + | 'address_components' + | 'adr_address' + | 'aspects' + | 'formatted_address' + | 'formatted_phone_number' + | 'geometry' + | 'html_attributions' + | 'icon' + | 'international_phone_number' + | 'name' + | 'opening_hours' + | 'photos' + | 'place_id' + | 'plus_code' + | 'price_level' + | 'rating' + | 'reviews' + | 'types' + | 'url' + | 'utc_offset' + | 'vicinity' + | 'website'; export type GoogleAutocompleteEvent = 'place_changed'; @@ -40,14 +40,14 @@ export interface GoogleAutocompleteWindow extends Window { } export type GoogleAddressFieldType = - 'postal_town' | - 'administrative_area_level_1' | - 'administrative_area_level_2' | - 'locality' | - 'neighborhood' | - 'postal_code' | - 'street_number' | - 'route' | - 'political' | - 'country' | - 'subpremise'; + | 'postal_town' + | 'administrative_area_level_1' + | 'administrative_area_level_2' + | 'locality' + | 'neighborhood' + | 'postal_code' + | 'street_number' + | 'route' + | 'political' + | 'country' + | 'subpremise'; diff --git a/packages/core/src/app/address/googleAutocomplete/mapToAddress.ts b/packages/core/src/app/address/googleAutocomplete/mapToAddress.ts index 727dd987ea..07c5109991 100644 --- a/packages/core/src/app/address/googleAutocomplete/mapToAddress.ts +++ b/packages/core/src/app/address/googleAutocomplete/mapToAddress.ts @@ -4,7 +4,7 @@ import AddressSelectorFactory from './AddressSelectorFactory'; export default function mapToAddress( autocompleteData: google.maps.places.PlaceResult, - countries: Country[] = [] + countries: Country[] = [], ): Partial
    { if (!autocompleteData || !autocompleteData.address_components) { return {}; @@ -13,7 +13,7 @@ export default function mapToAddress( const accessor = AddressSelectorFactory.create(autocompleteData); const state = accessor.getState(); const countryCode = accessor.getCountry(); - const country = countries && countries.find(c => countryCode === c.code); + const country = countries && countries.find((c) => countryCode === c.code); const street2 = accessor.getStreet2(); return { @@ -21,17 +21,12 @@ export default function mapToAddress( city: accessor.getCity(), countryCode, postalCode: accessor.getPostCode(), - ...state ? getState(state, country && country.subdivisions) : {}, + ...(state ? getState(state, country && country.subdivisions) : {}), }; } -function getState( - stateName: string, - states: Region[] = [] -): Partial
    { - const state = states.find(({ code, name }: Region) => - code === stateName || name === stateName - ); +function getState(stateName: string, states: Region[] = []): Partial
    { + const state = states.find(({ code, name }: Region) => code === stateName || name === stateName); if (!state) { return { diff --git a/packages/core/src/app/address/index.ts b/packages/core/src/app/address/index.ts index a52337414e..30b0d60a5b 100644 --- a/packages/core/src/app/address/index.ts +++ b/packages/core/src/app/address/index.ts @@ -12,4 +12,4 @@ export { default as isEqualAddress } from './isEqualAddress'; export { default as getAddressFormFieldsValidationSchema, getTranslateAddressError, - } from './getAddressFormFieldsValidationSchema'; +} from './getAddressFormFieldsValidationSchema'; diff --git a/packages/core/src/app/address/isEqualAddress.spec.ts b/packages/core/src/app/address/isEqualAddress.spec.ts index 6c908c599e..22d9ca0f4f 100644 --- a/packages/core/src/app/address/isEqualAddress.spec.ts +++ b/packages/core/src/app/address/isEqualAddress.spec.ts @@ -6,84 +6,113 @@ import mapAddressToFormValues from './mapAddressToFormValues'; describe('isEqualAddress', () => { it('returns true when ignored values are different', () => { - expect(isEqualAddress(getAddress(), { - ...getAddress(), - stateOrProvinceCode: 'w', - country: 'none', - id: 'x', - email: 'y', - shouldSaveAddress: false, - type: 'z', - })).toBeTruthy(); + expect( + isEqualAddress(getAddress(), { + ...getAddress(), + stateOrProvinceCode: 'w', + country: 'none', + id: 'x', + email: 'y', + shouldSaveAddress: false, + type: 'z', + }), + ).toBeTruthy(); }); it('returns true when stateOrProvinceCode matches stateOrProvince', () => { - expect(isEqualAddress({ - ...getAddress(), - stateOrProvince: '', - stateOrProvinceCode: 'CA', - }, { - ...getAddress(), - stateOrProvince: 'California', - stateOrProvinceCode: 'CA', - })).toBeTruthy(); + expect( + isEqualAddress( + { + ...getAddress(), + stateOrProvince: '', + stateOrProvinceCode: 'CA', + }, + { + ...getAddress(), + stateOrProvince: 'California', + stateOrProvinceCode: 'CA', + }, + ), + ).toBeTruthy(); - expect(isEqualAddress({ - ...getAddress(), - stateOrProvince: 'California', - stateOrProvinceCode: '', - }, { - ...getAddress(), - stateOrProvince: 'California', - stateOrProvinceCode: 'CA', - })).toBeTruthy(); + expect( + isEqualAddress( + { + ...getAddress(), + stateOrProvince: 'California', + stateOrProvinceCode: '', + }, + { + ...getAddress(), + stateOrProvince: 'California', + stateOrProvinceCode: 'CA', + }, + ), + ).toBeTruthy(); - expect(isEqualAddress({ - ...getAddress(), - stateOrProvince: '', - stateOrProvinceCode: '', - }, { - ...getAddress(), - stateOrProvince: '', - stateOrProvinceCode: '', - })).toBeTruthy(); + expect( + isEqualAddress( + { + ...getAddress(), + stateOrProvince: '', + stateOrProvinceCode: '', + }, + { + ...getAddress(), + stateOrProvince: '', + stateOrProvinceCode: '', + }, + ), + ).toBeTruthy(); }); it('returns false when values are different', () => { - expect(isEqualAddress({ - ...getAddress(), - stateOrProvince: 'California', - stateOrProvinceCode: '', - }, { - ...getAddress(), - stateOrProvince: 'New York', - stateOrProvinceCode: '', - })).toBeFalsy(); + expect( + isEqualAddress( + { + ...getAddress(), + stateOrProvince: 'California', + stateOrProvinceCode: '', + }, + { + ...getAddress(), + stateOrProvince: 'New York', + stateOrProvinceCode: '', + }, + ), + ).toBeFalsy(); - expect(isEqualAddress({ - ...getAddress(), - stateOrProvince: '', - stateOrProvinceCode: 'CA', - }, { - ...getAddress(), - stateOrProvince: '', - stateOrProvinceCode: 'NY', - })).toBeFalsy(); + expect( + isEqualAddress( + { + ...getAddress(), + stateOrProvince: '', + stateOrProvinceCode: 'CA', + }, + { + ...getAddress(), + stateOrProvince: '', + stateOrProvinceCode: 'NY', + }, + ), + ).toBeFalsy(); }); it('returns true for transformed address', () => { const address = getAddress(); const transformedAddress = mapAddressFromFormValues( - mapAddressToFormValues(getAddressFormFields(), address) + mapAddressToFormValues(getAddressFormFields(), address), ); expect(isEqualAddress(address, transformedAddress)).toBeTruthy(); }); it('returns true when when custom fields are empty', () => { - expect(isEqualAddress(getAddress(), { - ...getAddress(), - customFields: [{ fieldId: 'foo', fieldValue: '' }], - })).toBeTruthy(); + expect( + isEqualAddress(getAddress(), { + ...getAddress(), + customFields: [{ fieldId: 'foo', fieldValue: '' }], + }), + ).toBeTruthy(); }); }); diff --git a/packages/core/src/app/address/isEqualAddress.ts b/packages/core/src/app/address/isEqualAddress.ts index 53e90826e7..2390a3b353 100644 --- a/packages/core/src/app/address/isEqualAddress.ts +++ b/packages/core/src/app/address/isEqualAddress.ts @@ -1,18 +1,26 @@ -import { Address, AddressRequestBody, BillingAddress, CustomerAddress } from '@bigcommerce/checkout-sdk'; +import { + Address, + AddressRequestBody, + BillingAddress, + CustomerAddress, +} from '@bigcommerce/checkout-sdk'; import { isEqual, omit } from 'lodash'; type ComparableAddress = CustomerAddress | Address | BillingAddress | AddressRequestBody; type ComparableAddressFields = keyof CustomerAddress | keyof Address | keyof BillingAddress; -export default function isEqualAddress(address1?: ComparableAddress, address2?: ComparableAddress): boolean { +export default function isEqualAddress( + address1?: ComparableAddress, + address2?: ComparableAddress, +): boolean { if (!address1 || !address2) { return false; } - return isEqual( - normalizeAddress(address1), - normalizeAddress(address2) - ) && isSameState(address1, address2); + return ( + isEqual(normalizeAddress(address1), normalizeAddress(address2)) && + isSameState(address1, address2) + ); } function isSameState(address1: ComparableAddress, address2: ComparableAddress): boolean { @@ -20,12 +28,17 @@ function isSameState(address1: ComparableAddress, address2: ComparableAddress): return true; } - if (address1.stateOrProvinceCode && address1.stateOrProvinceCode === address2.stateOrProvinceCode) { + if ( + address1.stateOrProvinceCode && + address1.stateOrProvinceCode === address2.stateOrProvinceCode + ) { return true; } - return address1.stateOrProvince === address2.stateOrProvince && - address1.stateOrProvinceCode === address2.stateOrProvinceCode; + return ( + address1.stateOrProvince === address2.stateOrProvince && + address1.stateOrProvinceCode === address2.stateOrProvinceCode + ); } function normalizeAddress(address: ComparableAddress) { @@ -44,6 +57,6 @@ function normalizeAddress(address: ComparableAddress) { ...address, customFields: (address.customFields || []).filter(({ fieldValue }) => !!fieldValue), }, - ignoredFields + ignoredFields, ); } diff --git a/packages/core/src/app/address/isValidAddress.spec.ts b/packages/core/src/app/address/isValidAddress.spec.ts index 864686d540..21a2c42e0b 100644 --- a/packages/core/src/app/address/isValidAddress.spec.ts +++ b/packages/core/src/app/address/isValidAddress.spec.ts @@ -4,47 +4,37 @@ import isValidAddress from './isValidAddress'; describe('isValidAddress()', () => { it('returns true if all required fields are defined', () => { - expect(isValidAddress(getAddress(), getFormFields())) - .toEqual(true); + expect(isValidAddress(getAddress(), getFormFields())).toBe(true); }); it('returns false if some required fields are not defined', () => { - expect(isValidAddress({ ...getAddress(), address1: '' }, getFormFields())) - .toEqual(false); + expect(isValidAddress({ ...getAddress(), address1: '' }, getFormFields())).toBe(false); }); describe('when field is dropdown', () => { it('returns false if dropdown is required but not defined', () => { const output = isValidAddress( getAddress(), - getFormFields().map(field => ( - field.fieldType === 'dropdown' ? - { ...field, required: true } : - field - )) + getFormFields().map((field) => + field.fieldType === 'dropdown' ? { ...field, required: true } : field, + ), ); - expect(output) - .toEqual(false); + expect(output).toBe(false); }); it('returns true if dropdown is required and defined', () => { const output = isValidAddress( { ...getAddress(), - customFields: [ - { fieldId: 'field_27', fieldValue: '0' }, - ], + customFields: [{ fieldId: 'field_27', fieldValue: '0' }], }, - getFormFields().map(field => ( - field.type === 'array' ? - { ...field, required: true } : - field - )) + getFormFields().map((field) => + field.type === 'array' ? { ...field, required: true } : field, + ), ); - expect(output) - .toEqual(true); + expect(output).toBe(true); }); }); @@ -53,33 +43,27 @@ describe('isValidAddress()', () => { const output = isValidAddress( { ...getAddress(), - customFields: [ - { fieldId: 'field_31', fieldValue: 0 }, - ], + customFields: [{ fieldId: 'field_31', fieldValue: 0 }], }, - getFormFields().map(field => ( - field.type === 'integer' ? - { ...field, required: true } : - field - )) + getFormFields().map((field) => + field.type === 'integer' ? { ...field, required: true } : field, + ), ); - expect(output) - .toEqual(true); + expect(output).toBe(true); }); it('returns false if field is required and not defined', () => { const output = isValidAddress( getAddress(), - getFormFields().map(field => ( - (typeof field.type !== undefined && field.type === 'integer') ? - { ...field, required: true } : - field - )) + getFormFields().map((field) => + typeof field.type !== undefined && field.type === 'integer' + ? { ...field, required: true } + : field, + ), ); - expect(output) - .toEqual(false); + expect(output).toBe(false); }); }); }); diff --git a/packages/core/src/app/address/isValidCustomerAddress.ts b/packages/core/src/app/address/isValidCustomerAddress.ts index d8ba9a7a30..1bff236851 100644 --- a/packages/core/src/app/address/isValidCustomerAddress.ts +++ b/packages/core/src/app/address/isValidCustomerAddress.ts @@ -7,11 +7,11 @@ import isValidAddress from './isValidAddress'; export default function isValidCustomerAddress( address: Address | undefined, addresses: CustomerAddress[], - formFields: FormField[] + formFields: FormField[], ): boolean { if (!address || !isValidAddress(address, formFields)) { return false; } - return some(addresses, customerAddress => isEqualAddress(customerAddress, address)); + return some(addresses, (customerAddress) => isEqualAddress(customerAddress, address)); } diff --git a/packages/core/src/app/address/localizeAddress.spec.ts b/packages/core/src/app/address/localizeAddress.spec.ts index 7327f75681..92c32b2971 100644 --- a/packages/core/src/app/address/localizeAddress.spec.ts +++ b/packages/core/src/app/address/localizeAddress.spec.ts @@ -29,7 +29,7 @@ describe('localizeAddress', () => { }); it('keeps same value if unable to provinceCode', () => { - const countries = getCountries().map(country => ({ + const countries = getCountries().map((country) => ({ ...country, subdivisions: [], })); diff --git a/packages/core/src/app/address/localizeAddress.ts b/packages/core/src/app/address/localizeAddress.ts index 44724f984d..4a3aefaae3 100644 --- a/packages/core/src/app/address/localizeAddress.ts +++ b/packages/core/src/app/address/localizeAddress.ts @@ -5,11 +5,11 @@ import { LocalizedGeography } from '../geography'; const localizeAddress = ( address: T1, - countries?: Country[] + countries?: Country[], ): T1 & LocalizedGeography => { - const country = find(countries, { code: address.countryCode }); + const country = find(countries, { code: address.countryCode }); const states = !country || isEmpty(country.subdivisions) ? [] : country.subdivisions; - const state = find(states, { code: address.stateOrProvinceCode }); + const state = find(states, { code: address.stateOrProvinceCode }); return { ...address, diff --git a/packages/core/src/app/address/mapAddressFromFormValues.spec.ts b/packages/core/src/app/address/mapAddressFromFormValues.spec.ts index 6f55ea05e6..e928dcba21 100644 --- a/packages/core/src/app/address/mapAddressFromFormValues.spec.ts +++ b/packages/core/src/app/address/mapAddressFromFormValues.spec.ts @@ -12,8 +12,7 @@ describe('mapAddressFromFormValues', () => { customFields: {}, }; - expect(mapAddressFromFormValues(formValues)) - .toMatchObject(getShippingAddress()); + expect(mapAddressFromFormValues(formValues)).toMatchObject(getShippingAddress()); }); it('converts formats date values to YYYY-MM-DD format', () => { diff --git a/packages/core/src/app/address/mapAddressToFormValues.ts b/packages/core/src/app/address/mapAddressToFormValues.ts index afeeaf32f1..99955b6a1b 100644 --- a/packages/core/src/app/address/mapAddressToFormValues.ts +++ b/packages/core/src/app/address/mapAddressToFormValues.ts @@ -6,8 +6,11 @@ export type AddressFormValues = Pick { if (custom) { @@ -15,13 +18,18 @@ export default function mapAddressToFormValues(fields: FormField[], address?: Ad addressFormValues.customFields = {}; } - const field = address && + const field = + address && address.customFields && address.customFields.find(({ fieldId }) => fieldId === name); - const fieldValue = (field && field.fieldValue); + const fieldValue = field && field.fieldValue; - addressFormValues.customFields[name] = getValue(fieldType, fieldValue, defaultValue); + addressFormValues.customFields[name] = getValue( + fieldType, + fieldValue, + defaultValue, + ); return addressFormValues; } @@ -32,13 +40,12 @@ export default function mapAddressToFormValues(fields: FormField[], address?: Ad return addressFormValues; }, - {} as AddressFormValues + {} as AddressFormValues, ), - }); + }; - values.shouldSaveAddress = address && address.shouldSaveAddress !== undefined ? - address.shouldSaveAddress : - true; + values.shouldSaveAddress = + address && address.shouldSaveAddress !== undefined ? address.shouldSaveAddress : true; // Manually backfill stateOrProvince to avoid Formik warning (uncontrolled to controlled input) if (values.stateOrProvince === undefined) { @@ -52,7 +59,11 @@ export default function mapAddressToFormValues(fields: FormField[], address?: Ad return values; } -function getValue(fieldType?: string, fieldValue?: string | string[] | number, defaultValue?: string): string | string[] | number | Date | undefined { +function getValue( + fieldType?: string, + fieldValue?: string | string[] | number, + defaultValue?: string, +): string | string[] | number | Date | undefined { if (fieldValue === undefined || fieldValue === null) { return getDefaultValue(fieldType, defaultValue); } @@ -76,6 +87,8 @@ function getDefaultValue(fieldType?: string, defaultValue?: string): string | st return defaultValue || ''; } -function isSystemAddressFieldName(fieldName: string): fieldName is Exclude { +function isSystemAddressFieldName( + fieldName: string, +): fieldName is Exclude { return fieldName !== 'customFields' && fieldName !== 'shouldSaveAddress'; } diff --git a/packages/core/src/app/auto-loader.ts b/packages/core/src/app/auto-loader.ts index 2f4ecaee34..1e04a321f7 100644 --- a/packages/core/src/app/auto-loader.ts +++ b/packages/core/src/app/auto-loader.ts @@ -23,16 +23,9 @@ function isCustomCheckoutWindow(window: Window): window is CustomCheckoutWindow throw new Error('Checkout config is missing.'); } - const { - renderOrderConfirmation, - renderCheckout, - } = await loadFiles(); - - const { - orderId, - checkoutId, - ...appProps - } = window.checkoutConfig; + const { renderOrderConfirmation, renderCheckout } = await loadFiles(); + + const { orderId, checkoutId, ...appProps } = window.checkoutConfig; if (orderId) { renderOrderConfirmation({ ...appProps, orderId }); diff --git a/packages/core/src/app/billing/Billing.spec.tsx b/packages/core/src/app/billing/Billing.spec.tsx index f550682f4e..caf948c92f 100644 --- a/packages/core/src/app/billing/Billing.spec.tsx +++ b/packages/core/src/app/billing/Billing.spec.tsx @@ -1,4 +1,9 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService, LineItemMap } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, + LineItemMap, +} from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import React, { FunctionComponent } from 'react'; @@ -12,8 +17,8 @@ import { getCountries } from '../geography/countries.mock'; import { createLocaleContext, LocaleContext, LocaleContextType } from '../locale'; import { OrderComments } from '../orderComments'; -import { getBillingAddress } from './billingAddresses.mock'; import Billing, { BillingProps } from './Billing'; +import { getBillingAddress } from './billingAddresses.mock'; import BillingForm from './BillingForm'; describe('Billing Component', () => { @@ -36,47 +41,51 @@ describe('Billing Component', () => { onUnhandledError: jest.fn(), }; - jest.spyOn(checkoutService.getState().data, 'getBillingAddressFields') - .mockReturnValue(getFormFields()); + jest.spyOn(checkoutService.getState().data, 'getBillingAddressFields').mockReturnValue( + getFormFields(), + ); - jest.spyOn(checkoutService.getState().data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutService.getState().data, 'getConfig').mockReturnValue(getStoreConfig()); - jest.spyOn(checkoutService.getState().data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(checkoutService.getState().data, 'getCart').mockReturnValue(getCart()); - jest.spyOn(checkoutService.getState().data, 'getCheckout') - .mockReturnValue(getCheckout()); + jest.spyOn(checkoutService.getState().data, 'getCheckout').mockReturnValue(getCheckout()); - jest.spyOn(checkoutService.getState().data, 'getCustomer') - .mockReturnValue(getCustomer()); + jest.spyOn(checkoutService.getState().data, 'getCustomer').mockReturnValue(getCustomer()); - jest.spyOn(checkoutService.getState().data, 'getBillingCountries') - .mockReturnValue(getCountries()); + jest.spyOn(checkoutService.getState().data, 'getBillingCountries').mockReturnValue( + getCountries(), + ); - jest.spyOn(checkoutService.getState().statuses, 'isUpdatingBillingAddress') - .mockReturnValue(false); + jest.spyOn(checkoutService.getState().statuses, 'isUpdatingBillingAddress').mockReturnValue( + false, + ); - jest.spyOn(checkoutService.getState().data, 'getBillingAddress') - .mockReturnValue(billingAddress); + jest.spyOn(checkoutService.getState().data, 'getBillingAddress').mockReturnValue( + billingAddress, + ); - jest.spyOn(checkoutService, 'updateBillingAddress').mockResolvedValue({} as CheckoutSelectors); + jest.spyOn(checkoutService, 'updateBillingAddress').mockResolvedValue( + {} as CheckoutSelectors, + ); jest.spyOn(checkoutService, 'updateCheckout').mockResolvedValue({} as CheckoutSelectors); - jest.spyOn(checkoutService, 'loadBillingAddressFields').mockResolvedValue({} as CheckoutSelectors); + jest.spyOn(checkoutService, 'loadBillingAddressFields').mockResolvedValue( + {} as CheckoutSelectors, + ); - ComponentTest = props => ( - - - + ComponentTest = (props) => ( + + + ); }); beforeEach(async () => { - component = mount(); + component = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); }); it('loads billing fields', () => { @@ -88,73 +97,71 @@ describe('Billing Component', () => { }); it('renders header', () => { - expect(component.find('[data-test="billing-address-heading"]').text()) - .toEqual('Billing Address'); + expect(component.find('[data-test="billing-address-heading"]').text()).toBe( + 'Billing Address', + ); }); it('does not render order comments when there are physical items', () => { - expect(component.find(OrderComments).length) - .toEqual(0); + expect(component.find(OrderComments)).toHaveLength(0); }); it('renders order comments when there are no physical items', () => { - jest.spyOn(checkoutService.getState().data, 'getCart') - .mockReturnValue({ - ...getCart(), - lineItems: { physicalItems: [] } as unknown as LineItemMap, - }); + jest.spyOn(checkoutService.getState().data, 'getCart').mockReturnValue({ + ...getCart(), + lineItems: { physicalItems: [] } as unknown as LineItemMap, + }); - component = mount(); + component = mount(); - expect(component.find(OrderComments).length) - .toEqual(1); + expect(component.find(OrderComments)).toHaveLength(1); }); it('updates order comment when input value does not match state value', async () => { - jest.spyOn(checkoutService.getState().data, 'getCart') - .mockReturnValue({ - ...getCart(), - lineItems: { physicalItems: [] } as unknown as LineItemMap, - }); + jest.spyOn(checkoutService.getState().data, 'getCart').mockReturnValue({ + ...getCart(), + lineItems: { physicalItems: [] } as unknown as LineItemMap, + }); - component = mount(); + component = mount(); - component.find('input[name="orderComment"]') - .simulate('change', { target: { value: 'foo', name: 'orderComment' } }); + component + .find('input[name="orderComment"]') + .simulate('change', { target: { value: 'foo', name: 'orderComment' } }); - component.find('form') - .simulate('submit'); + component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); expect(checkoutService.updateCheckout).toHaveBeenCalledWith({ customerMessage: 'foo' }); expect(defaultProps.navigateNextStep).toHaveBeenCalled(); }); it('does not render BillingForm while loading billing countries', () => { - jest.spyOn(checkoutService.getState().statuses, 'isLoadingBillingCountries') - .mockReturnValue(true); + jest.spyOn( + checkoutService.getState().statuses, + 'isLoadingBillingCountries', + ).mockReturnValue(true); - component = mount(); + component = mount(); - expect(component.find(BillingForm).length) - .toEqual(0); + expect(component.find(BillingForm)).toHaveLength(0); }); it('renders BillingForm with expected props', () => { - expect(component.find(BillingForm).props()) - .toEqual(expect.objectContaining({ + expect(component.find(BillingForm).props()).toEqual( + expect.objectContaining({ billingAddress, customer: getCustomer(), countries: getCountries(), - })); + }), + ); }); it('calls updateBillingAddress and navigateNextStep when form is submitted and valid', async () => { - component.find('form') - .simulate('submit'); + component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); expect(checkoutService.updateBillingAddress).toHaveBeenCalledWith({ address1: '12345 Testing Way', @@ -177,15 +184,12 @@ describe('Billing Component', () => { it('calls unhandled error handler when there is error that is not handled by component', async () => { const error = new Error(); - jest.spyOn(checkoutService, 'updateBillingAddress') - .mockRejectedValue(error); + jest.spyOn(checkoutService, 'updateBillingAddress').mockRejectedValue(error); - component.find('form') - .simulate('submit'); + component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(defaultProps.onUnhandledError) - .toHaveBeenCalledWith(error); + expect(defaultProps.onUnhandledError).toHaveBeenCalledWith(error); }); }); diff --git a/packages/core/src/app/billing/Billing.tsx b/packages/core/src/app/billing/Billing.tsx index 3992a94623..123e00634e 100644 --- a/packages/core/src/app/billing/Billing.tsx +++ b/packages/core/src/app/billing/Billing.tsx @@ -1,17 +1,24 @@ -import { Address, CheckoutRequestBody, CheckoutSelectors, Country, Customer, FormField } from '@bigcommerce/checkout-sdk'; +import { + Address, + CheckoutRequestBody, + CheckoutSelectors, + Country, + Customer, + FormField, +} from '@bigcommerce/checkout-sdk'; import { noop } from 'lodash'; import React, { Component, ReactNode } from 'react'; import { isEqualAddress, mapAddressFromFormValues } from '../address'; -import { withCheckout, CheckoutContextProps } from '../checkout'; +import { CheckoutContextProps, withCheckout } from '../checkout'; import { EMPTY_ARRAY } from '../common/utility'; import { TranslatedString } from '../locale'; import { getShippableItemsCount } from '../shipping'; import { Legend } from '../ui/form'; import { LoadingOverlay } from '../ui/loading'; -import getBillingMethodId from './getBillingMethodId'; import BillingForm, { BillingFormValues } from './BillingForm'; +import getBillingMethodId from './getBillingMethodId'; export interface BillingProps { navigateNextStep(): void; @@ -38,11 +45,7 @@ export interface WithCheckoutBillingProps { class Billing extends Component { async componentDidMount(): Promise { - const { - initialize, - onReady = noop, - onUnhandledError, - } = this.props; + const { initialize, onReady = noop, onUnhandledError } = this.props; try { await initialize(); @@ -55,11 +58,7 @@ class Billing extends Component { } render(): ReactNode { - const { - updateAddress, - isInitializing, - ...props - } = this.props; + const { updateAddress, isInitializing, ...props } = this.props; return (
    @@ -69,14 +68,11 @@ class Billing extends Component {
    - +
    @@ -133,11 +129,7 @@ function mapToBillingProps({ getBillingAddressFields, getBillingCountries, }, - statuses: { - isLoadingBillingCountries, - isUpdatingBillingAddress, - isUpdatingCheckout, - }, + statuses: { isLoadingBillingCountries, isUpdatingBillingAddress, isUpdatingCheckout }, } = checkoutState; const config = getConfig(); @@ -149,11 +141,7 @@ function mapToBillingProps({ return null; } - const { - enableOrderComments, - googleMapsApiKey, - features, - } = config.checkoutSettings; + const { enableOrderComments, googleMapsApiKey, features } = config.checkoutSettings; const countriesWithAutocomplete = ['US', 'CA', 'AU', 'NZ']; diff --git a/packages/core/src/app/billing/BillingForm.spec.tsx b/packages/core/src/app/billing/BillingForm.spec.tsx index 2e38062e50..382b1daf0c 100644 --- a/packages/core/src/app/billing/BillingForm.spec.tsx +++ b/packages/core/src/app/billing/BillingForm.spec.tsx @@ -43,14 +43,14 @@ describe('BillingForm Component', () => { beforeEach(() => { component = mount( - - - + + + , ); }); it('renders form with expected id', () => { - expect(component.find('fieldset#checkoutBillingAddress').length).toEqual(1); + expect(component.find('fieldset#checkoutBillingAddress')).toHaveLength(1); }); it('renders form with static address and custom fields', () => { @@ -60,63 +60,66 @@ describe('BillingForm Component', () => { }; component = mount( - - - + + + , ); - expect(component.find(StaticBillingAddress).length).toEqual(1); - expect(component.find(AddressSelect).length).toEqual(0); - expect(component.find(DynamicFormField).length).toEqual(4); + expect(component.find(StaticBillingAddress)).toHaveLength(1); + expect(component.find(AddressSelect)).toHaveLength(0); + expect(component.find(DynamicFormField)).toHaveLength(4); }); it('renders addresses', () => { - expect(component.find('fieldset#billingAddresses').length).toEqual(1); - expect(component.find(AddressSelect).props()).toEqual(expect.objectContaining({ - addresses: getCustomer().addresses, - })); + expect(component.find('fieldset#billingAddresses')).toHaveLength(1); + expect(component.find(AddressSelect).props()).toEqual( + expect.objectContaining({ + addresses: getCustomer().addresses, + }), + ); }); it('does not render address form when selected customer address is valid', () => { component = mount( - + - + , ); - expect(component.find(AddressForm).length).toEqual(0); + expect(component.find(AddressForm)).toHaveLength(0); }); it('renders address form when selected customer address is not valid', () => { component = mount( - + - + , ); - expect(component.find(AddressForm).length).toEqual(1); + expect(component.find(AddressForm)).toHaveLength(1); }); it('renders address form', () => { - expect(component.find(AddressForm).props()).toEqual(expect.objectContaining({ - countries: getCountries(), - })); + expect(component.find(AddressForm).props()).toEqual( + expect.objectContaining({ + countries: getCountries(), + }), + ); }); it('calls handle submit when form is submitted and valid', async () => { - component.find('form') - .simulate('submit'); + component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); expect(defaultProps.onSubmit).toHaveBeenCalledWith({ address1: '12345 Testing Way', @@ -136,13 +139,13 @@ describe('BillingForm Component', () => { }); it('calls does not call handle submit when form is submitted and invalid', async () => { - component.find('input#firstNameInput') + component + .find('input#firstNameInput') .simulate('change', { target: { value: '', name: 'firstName' } }); - component.find('form') - .simulate('submit'); + component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); expect(defaultProps.onSubmit).not.toHaveBeenCalled(); }); diff --git a/packages/core/src/app/billing/BillingForm.tsx b/packages/core/src/app/billing/BillingForm.tsx index b5eecb6843..75b209750a 100644 --- a/packages/core/src/app/billing/BillingForm.tsx +++ b/packages/core/src/app/billing/BillingForm.tsx @@ -1,11 +1,25 @@ -import { Address, CheckoutSelectors, Country, Customer, FormField } from '@bigcommerce/checkout-sdk'; -import { withFormik, FormikProps } from 'formik'; +import { + Address, + CheckoutSelectors, + Country, + Customer, + FormField, +} from '@bigcommerce/checkout-sdk'; +import { FormikProps, withFormik } from 'formik'; import React, { createRef, PureComponent, ReactNode, RefObject } from 'react'; import { lazy } from 'yup'; -import { getAddressFormFieldsValidationSchema, getTranslateAddressError, isValidCustomerAddress, mapAddressToFormValues, AddressForm, AddressFormValues, AddressSelect } from '../address'; +import { + AddressForm, + AddressFormValues, + AddressSelect, + getAddressFormFieldsValidationSchema, + getTranslateAddressError, + isValidCustomerAddress, + mapAddressToFormValues, +} from '../address'; import { getCustomFormFieldsValidationSchema } from '../formFields'; -import { withLanguage, TranslatedString, WithLanguageProps } from '../locale'; +import { TranslatedString, withLanguage, WithLanguageProps } from '../locale'; import { OrderComments } from '../orderComments'; import { Button, ButtonVariant } from '../ui/button'; import { Fieldset, Form } from '../ui/form'; @@ -35,7 +49,10 @@ interface BillingFormState { isResettingAddress: boolean; } -class BillingForm extends PureComponent, BillingFormState> { +class BillingForm extends PureComponent< + BillingFormProps & WithLanguageProps & FormikProps, + BillingFormState +> { state: BillingFormState = { isResettingAddress: false, }; @@ -61,56 +78,66 @@ class BillingForm extends PureComponent custom); const hasCustomFormFields = customFormFields.length > 0; - const editableFormFields = shouldRenderStaticAddress && hasCustomFormFields ? customFormFields : allFormFields; + const editableFormFields = + shouldRenderStaticAddress && hasCustomFormFields ? customFormFields : allFormFields; const { isResettingAddress } = this.state; const hasAddresses = addresses && addresses.length > 0; - const hasValidCustomerAddress = billingAddress && - isValidCustomerAddress(billingAddress, addresses, getFields(billingAddress.countryCode)); + const hasValidCustomerAddress = + billingAddress && + isValidCustomerAddress( + billingAddress, + addresses, + getFields(billingAddress.countryCode), + ); return ( - { shouldRenderStaticAddress && billingAddress && -
    - -
    } - -
    - { hasAddresses && !shouldRenderStaticAddress && + {shouldRenderStaticAddress && billingAddress && ( +
    + +
    + )} + +
    + {hasAddresses && !shouldRenderStaticAddress && (
    - + -
    } +
    + )} - { !hasValidCustomerAddress && - + {!hasValidCustomerAddress && ( + - } + + )}
    - { shouldShowOrderComments && - } + {shouldShowOrderComments && }
    @@ -119,11 +146,8 @@ class BillingForm extends PureComponent) => void = async address => { - const { - updateAddress, - onUnhandledError, - } = this.props; + private handleSelectAddress: (address: Partial
    ) => void = async (address) => { + const { updateAddress, onUnhandledError } = this.props; this.setState({ isResettingAddress: true }); @@ -143,40 +167,42 @@ class BillingForm extends PureComponent({ - handleSubmit: (values, { props: { onSubmit } }) => { - onSubmit(values); - }, - mapPropsToValues: ({ getFields, customerMessage, billingAddress }) => ( - { - ...mapAddressToFormValues( - getFields(billingAddress && billingAddress.countryCode), - billingAddress - ), - orderComment: customerMessage, - }), - isInitialValid: ({ - billingAddress, - getFields, - language, - }) => ( - !!billingAddress && getAddressFormFieldsValidationSchema({ +export default withLanguage( + withFormik({ + handleSubmit: (values, { props: { onSubmit } }) => { + onSubmit(values); + }, + mapPropsToValues: ({ getFields, customerMessage, billingAddress }) => ({ + ...mapAddressToFormValues( + getFields(billingAddress && billingAddress.countryCode), + billingAddress, + ), + orderComment: customerMessage, + }), + isInitialValid: ({ billingAddress, getFields, language }) => + !!billingAddress && + getAddressFormFieldsValidationSchema({ + language, + formFields: getFields(billingAddress.countryCode), + }).isValidSync(billingAddress), + validationSchema: ({ language, - formFields: getFields(billingAddress.countryCode), - }).isValidSync(billingAddress) - ), - validationSchema: ({ - language, - getFields, - methodId, - }: BillingFormProps & WithLanguageProps) => methodId === 'amazonpay' ? - (lazy>(values => getCustomFormFieldsValidationSchema({ - translate: getTranslateAddressError(language), - formFields: getFields(values && values.countryCode), - }))) : - (lazy>(values => getAddressFormFieldsValidationSchema({ - language, - formFields: getFields(values && values.countryCode), - }))), - enableReinitialize: true, -})(BillingForm)); + getFields, + methodId, + }: BillingFormProps & WithLanguageProps) => + methodId === 'amazonpay' + ? lazy>((values) => + getCustomFormFieldsValidationSchema({ + translate: getTranslateAddressError(language), + formFields: getFields(values && values.countryCode), + }), + ) + : lazy>((values) => + getAddressFormFieldsValidationSchema({ + language, + formFields: getFields(values && values.countryCode), + }), + ), + enableReinitialize: true, + })(BillingForm), +); diff --git a/packages/core/src/app/billing/StaticBillingAddress.spec.tsx b/packages/core/src/app/billing/StaticBillingAddress.spec.tsx index d74bb24ae9..30076ce12d 100644 --- a/packages/core/src/app/billing/StaticBillingAddress.spec.tsx +++ b/packages/core/src/app/billing/StaticBillingAddress.spec.tsx @@ -1,4 +1,8 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, +} from '@bigcommerce/checkout-sdk'; import { mount } from 'enzyme'; import React, { FunctionComponent } from 'react'; @@ -25,65 +29,58 @@ describe('StaticBillingAddress', () => { address: getAddress(), }; - jest.spyOn(checkoutState.data, 'getCheckout') - .mockReturnValue(getCheckout()); + jest.spyOn(checkoutState.data, 'getCheckout').mockReturnValue(getCheckout()); - jest.spyOn(checkoutState.data, 'getBillingAddressFields') - .mockReturnValue(getAddressFormFields()); + jest.spyOn(checkoutState.data, 'getBillingAddressFields').mockReturnValue( + getAddressFormFields(), + ); - StaticBillingAddressTest = props => ( - - - + StaticBillingAddressTest = (props) => ( + + + ); }); it('renders address normally if not using Amazon', () => { - const container = mount(); + const container = mount(); - expect(container.find(StaticAddress).length) - .toEqual(1); + expect(container.find(StaticAddress)).toHaveLength(1); }); it('renders message instead of address when using Amazon', () => { - jest.spyOn(checkoutState.data, 'getCheckout') - .mockReturnValue({ - ...getCheckout(), - payments: [ - { ...getCheckoutPayment(), providerId: 'amazon' }, - ], - }); + jest.spyOn(checkoutState.data, 'getCheckout').mockReturnValue({ + ...getCheckout(), + payments: [{ ...getCheckoutPayment(), providerId: 'amazon' }], + }); - const container = mount(); + const container = mount(); - expect(container.find(StaticAddress).length) - .toEqual(0); + expect(container.find(StaticAddress)).toHaveLength(0); - expect(container.text()) - .toEqual(getLanguageService().translate('billing.billing_address_amazon')); + expect(container.text()).toEqual( + getLanguageService().translate('billing.billing_address_amazon'), + ); }); it('renders message instead of address when using Amazon Pay V2 and no full address is provided', () => { - jest.spyOn(checkoutState.data, 'getCheckout') - .mockReturnValue({ - ...getCheckout(), - payments: [ - { ...getCheckoutPayment(), providerId: 'amazonpay' }, - ], - }); + jest.spyOn(checkoutState.data, 'getCheckout').mockReturnValue({ + ...getCheckout(), + payments: [{ ...getCheckoutPayment(), providerId: 'amazonpay' }], + }); const addressData = { ...getAddress(), }; - const container = mount(); + const container = mount(); - expect(container.find(StaticAddress).length) - .toEqual(0); + expect(container.find(StaticAddress)).toHaveLength(0); - expect(container.text()) - .toEqual(getLanguageService().translate('billing.billing_address_amazonpay')); + expect(container.text()).toEqual( + getLanguageService().translate('billing.billing_address_amazonpay'), + ); }); }); diff --git a/packages/core/src/app/billing/StaticBillingAddress.tsx b/packages/core/src/app/billing/StaticBillingAddress.tsx index b1658f7975..556bfc4c95 100644 --- a/packages/core/src/app/billing/StaticBillingAddress.tsx +++ b/packages/core/src/app/billing/StaticBillingAddress.tsx @@ -1,8 +1,8 @@ import { Address, CheckoutPayment, FormField } from '@bigcommerce/checkout-sdk'; -import React, { memo, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo } from 'react'; import { AddressType, StaticAddress } from '../address'; -import { withCheckout, CheckoutContextProps } from '../checkout'; +import { CheckoutContextProps, withCheckout } from '../checkout'; import { EMPTY_ARRAY } from '../common/utility'; import { TranslatedString } from '../locale'; @@ -16,41 +16,33 @@ interface WithCheckoutStaticBillingAddressProps { } const StaticBillingAddress: FunctionComponent< - StaticBillingAddressProps & - WithCheckoutStaticBillingAddressProps -> = ({ - address, - payments = EMPTY_ARRAY, -}) => { - if (payments.find(payment => payment.providerId === 'amazon')) { + StaticBillingAddressProps & WithCheckoutStaticBillingAddressProps +> = ({ address, payments = EMPTY_ARRAY }) => { + if (payments.find((payment) => payment.providerId === 'amazon')) { return ( -

    +

    + +

    ); } - if (payments.find(payment => payment.providerId === 'amazonpay')) { + if (payments.find((payment) => payment.providerId === 'amazonpay')) { return ( -

    +

    + +

    ); } - return ( - - ); + return ; }; export function mapToStaticBillingAddressProps( { checkoutState }: CheckoutContextProps, - { address }: StaticBillingAddressProps + { address }: StaticBillingAddressProps, ): WithCheckoutStaticBillingAddressProps | null { const { - data: { - getBillingAddressFields, - getCheckout, - }, + data: { getBillingAddressFields, getCheckout }, } = checkoutState; const checkout = getCheckout(); diff --git a/packages/core/src/app/billing/getBillingMethodId.ts b/packages/core/src/app/billing/getBillingMethodId.ts index e0c067a0c8..135a062480 100644 --- a/packages/core/src/app/billing/getBillingMethodId.ts +++ b/packages/core/src/app/billing/getBillingMethodId.ts @@ -6,7 +6,7 @@ export default function getBillingMethodId(checkout: Checkout): string | undefin const BILLING_METHOD_IDS = ['amazonpay']; const preselectedPayment = getPreselectedPayment(checkout); - return preselectedPayment && BILLING_METHOD_IDS.indexOf(preselectedPayment.providerId) > -1 ? - preselectedPayment.providerId : - undefined; + return preselectedPayment && BILLING_METHOD_IDS.indexOf(preselectedPayment.providerId) > -1 + ? preselectedPayment.providerId + : undefined; } diff --git a/packages/core/src/app/cart/AppliedRedeemable.spec.tsx b/packages/core/src/app/cart/AppliedRedeemable.spec.tsx index 46ef73ebad..591c87bdbc 100644 --- a/packages/core/src/app/cart/AppliedRedeemable.spec.tsx +++ b/packages/core/src/app/cart/AppliedRedeemable.spec.tsx @@ -13,22 +13,18 @@ describe('AppliedGiftCertificate', () => { beforeEach(() => { const localeContext = createLocaleContext(getStoreConfig()); const AppliedRedeembleContainer = ({ isRemoving }: { isRemoving: boolean }) => ( - - + + foo ); - component = mount(); + component = mount(); }); it('renders children', () => { - expect(component.find('.redeemable').text()) - .toEqual('foo'); + expect(component.find('.redeemable').text()).toBe('foo'); }); it('renders children', () => { diff --git a/packages/core/src/app/cart/AppliedRedeemable.tsx b/packages/core/src/app/cart/AppliedRedeemable.tsx index afba3e0e9f..9e51f53f2f 100644 --- a/packages/core/src/app/cart/AppliedRedeemable.tsx +++ b/packages/core/src/app/cart/AppliedRedeemable.tsx @@ -25,16 +25,13 @@ const AppliedRedeemable: FunctionComponent = ({
    - { children } + {children}
    -
    - - ), [appliedRedeemableError, handleKeyDown, handleSubmit, isApplyingRedeemable, language, renderErrorMessage]); - - const renderContent = useCallback(memoizeOne(({ setSubmitted }: FormContextType) => ( - - )), [ - renderLabel, - renderInput, - ]); - - return
    - - { renderContent } - -
    ; + const renderInput = useCallback( + (setSubmitted: FormContextType['setSubmitted']) => + ({ field }: FieldProps) => + ( + <> + {appliedRedeemableError && + appliedRedeemableError.errors && + appliedRedeemableError.errors[0] && ( + + {renderErrorMessage(appliedRedeemableError.errors[0].code)} + + )} + +
    + + + +
    + + ), + [ + appliedRedeemableError, + handleKeyDown, + handleSubmit, + isApplyingRedeemable, + language, + renderErrorMessage, + ], + ); + + const renderContent = useCallback( + memoizeOne(({ setSubmitted }: FormContextType) => ( + + )), + [renderLabel, renderInput], + ); + + return ( +
    + {renderContent} +
    + ); }; -export default withLanguage(withFormik({ - mapPropsToValues() { - return { - redeemableCode: '', - }; - }, - - async handleSubmit({ redeemableCode }, { props: { applyCoupon, applyGiftCertificate, clearError } }) { - const code = redeemableCode.trim(); - - try { - await applyGiftCertificate(code); - } catch (error) { - if (error instanceof Error) { - clearError(error); +export default withLanguage( + withFormik({ + mapPropsToValues() { + return { + redeemableCode: '', + }; + }, + + async handleSubmit( + { redeemableCode }, + { props: { applyCoupon, applyGiftCertificate, clearError } }, + ) { + const code = redeemableCode.trim(); + + try { + await applyGiftCertificate(code); + } catch (error) { + if (error instanceof Error) { + clearError(error); + } + + applyCoupon(code); } - applyCoupon(code); - } - }, - - validationSchema({ language }: RedeemableProps & WithLanguageProps) { - return object({ - redeemableCode: string() - .required(language.translate('redeemable.code_required_error')), - }); - }, -})(memo(Redeemable))); + }, + + validationSchema({ language }: RedeemableProps & WithLanguageProps) { + return object({ + redeemableCode: string().required( + language.translate('redeemable.code_required_error'), + ), + }); + }, + })(memo(Redeemable)), +); diff --git a/packages/core/src/app/cart/carts.mock.ts b/packages/core/src/app/cart/carts.mock.ts index 4695ca599d..cf0ad925ac 100644 --- a/packages/core/src/app/cart/carts.mock.ts +++ b/packages/core/src/app/cart/carts.mock.ts @@ -21,12 +21,8 @@ export function getCart(): Cart { coupons: [], discounts: [], lineItems: { - physicalItems: [ - getPhysicalItem(), - ], - digitalItems: [ - getDigitalItem(), - ], + physicalItems: [getPhysicalItem()], + digitalItems: [getDigitalItem()], giftCertificates: [], customItems: [], }, diff --git a/packages/core/src/app/cart/lineItem.mock.ts b/packages/core/src/app/cart/lineItem.mock.ts index d3c90d0080..0f206160a2 100644 --- a/packages/core/src/app/cart/lineItem.mock.ts +++ b/packages/core/src/app/cart/lineItem.mock.ts @@ -1,4 +1,9 @@ -import { CustomItem, DigitalItem, GiftCertificateItem, PhysicalItem } from '@bigcommerce/checkout-sdk'; +import { + CustomItem, + DigitalItem, + GiftCertificateItem, + PhysicalItem, +} from '@bigcommerce/checkout-sdk'; export function getCustomItem(): CustomItem { return { @@ -135,7 +140,8 @@ export function getPicklistItem(): PhysicalItem[] { }, ], categoryNames: ['Cat 1'], - }, { + }, + { id: '777', variantId: 72, productId: 104, diff --git a/packages/core/src/app/cart/mapToCartSummaryProps.ts b/packages/core/src/app/cart/mapToCartSummaryProps.ts index db166c30df..497fb1d25a 100644 --- a/packages/core/src/app/cart/mapToCartSummaryProps.ts +++ b/packages/core/src/app/cart/mapToCartSummaryProps.ts @@ -1,10 +1,10 @@ import { CheckoutContextProps } from '../checkout'; -import mapToRedeemableProps from './mapToRedeemableProps'; import { WithCheckoutCartSummaryProps } from './CartSummary'; +import mapToRedeemableProps from './mapToRedeemableProps'; export default function mapToCartSummaryProps( - context: CheckoutContextProps + context: CheckoutContextProps, ): WithCheckoutCartSummaryProps | null { const { checkoutState: { diff --git a/packages/core/src/app/cart/mapToOrderSummarySubtotalsProps.ts b/packages/core/src/app/cart/mapToOrderSummarySubtotalsProps.ts index ca81c656ba..55a594e3b9 100644 --- a/packages/core/src/app/cart/mapToOrderSummarySubtotalsProps.ts +++ b/packages/core/src/app/cart/mapToOrderSummarySubtotalsProps.ts @@ -19,9 +19,9 @@ export default function mapToOrderSummarySubtotalsProps({ discountAmount, giftCertificates, giftWrappingAmount: giftWrappingCostTotal, - shippingAmount: hasSelectedShippingOptions(consignments) ? - shippingCostBeforeDiscount : - undefined, + shippingAmount: hasSelectedShippingOptions(consignments) + ? shippingCostBeforeDiscount + : undefined, handlingAmount: handlingCostTotal, coupons, taxes, diff --git a/packages/core/src/app/cart/mapToRedeemableProps.ts b/packages/core/src/app/cart/mapToRedeemableProps.ts index 740a5a5382..edf9391d41 100644 --- a/packages/core/src/app/cart/mapToRedeemableProps.ts +++ b/packages/core/src/app/cart/mapToRedeemableProps.ts @@ -4,16 +4,12 @@ import { EMPTY_ARRAY } from '../common/utility'; import { RedeemableProps } from './Redeemable'; export default function mapToRedeemableProps( - context: CheckoutContextProps + context: CheckoutContextProps, ): RedeemableProps | null { const { checkoutService, checkoutState: { - data: { - getConfig, - getCoupons, - getGiftCertificates, - }, + data: { getConfig, getCoupons, getGiftCertificates }, statuses: { isApplyingCoupon, isApplyingGiftCertificate, @@ -42,12 +38,12 @@ export default function mapToRedeemableProps( clearError: checkoutService.clearError, coupons: getCoupons() || EMPTY_ARRAY, giftCertificates: getGiftCertificates() || EMPTY_ARRAY, - isApplyingRedeemable: (isApplyingCoupon() || isApplyingGiftCertificate()), + isApplyingRedeemable: isApplyingCoupon() || isApplyingGiftCertificate(), isRemovingCoupon: isRemovingCoupon(), isRemovingGiftCertificate: isRemovingGiftCertificate(), onRemovedCoupon: checkoutService.removeCoupon, onRemovedGiftCertificate: checkoutService.removeGiftCertificate, - removedRedeemableError: (getRemoveCouponError() || getRemoveGiftCertificateError()), + removedRedeemableError: getRemoveCouponError() || getRemoveGiftCertificateError(), shouldCollapseCouponCode: config.checkoutSettings.isCouponCodeCollapsed, }; } diff --git a/packages/core/src/app/cart/withRedeemable.tsx b/packages/core/src/app/cart/withRedeemable.tsx index 6eb76603d7..3d969b9038 100644 --- a/packages/core/src/app/cart/withRedeemable.tsx +++ b/packages/core/src/app/cart/withRedeemable.tsx @@ -2,17 +2,14 @@ import React, { ComponentType, FunctionComponent } from 'react'; import { OrderSummaryProps, OrderSummarySubtotalsProps } from '../order'; -import mapToOrderSummarySubtotalsProps from './mapToOrderSummarySubtotalsProps'; import { WithCheckoutCartSummaryProps } from './CartSummary'; +import mapToOrderSummarySubtotalsProps from './mapToOrderSummarySubtotalsProps'; import Redeemable from './Redeemable'; export default function withRedeemable( - OriginalComponent: ComponentType -): FunctionComponent< - WithCheckoutCartSummaryProps & - { headerLink?: any } -> { - return props => { + OriginalComponent: ComponentType, +): FunctionComponent { + return (props) => { const { checkout, storeCurrency, @@ -26,23 +23,24 @@ export default function withRedeemable( return ( } - headerLink={ headerLink } - lineItems={ checkout.cart.lineItems } - onRemovedCoupon={ onRemovedCoupon } - onRemovedGiftCertificate={ onRemovedGiftCertificate } - shopperCurrency={ shopperCurrency } - storeCreditAmount={ storeCreditAmount } - storeCurrency={ storeCurrency } - total={ checkout.outstandingBalance } + headerLink={headerLink} + lineItems={checkout.cart.lineItems} + onRemovedCoupon={onRemovedCoupon} + onRemovedGiftCertificate={onRemovedGiftCertificate} + shopperCurrency={shopperCurrency} + storeCreditAmount={storeCreditAmount} + storeCurrency={storeCurrency} + total={checkout.outstandingBalance} /> ); }; diff --git a/packages/core/src/app/checkout/Checkout.spec.tsx b/packages/core/src/app/checkout/Checkout.spec.tsx index 72260b1f56..7983c678bf 100644 --- a/packages/core/src/app/checkout/Checkout.spec.tsx +++ b/packages/core/src/app/checkout/Checkout.spec.tsx @@ -1,4 +1,12 @@ -import { createCheckoutService, createEmbeddedCheckoutMessenger, CheckoutSelectors, CheckoutService, EmbeddedCheckoutMessenger, StepTracker, BodlService } from '@bigcommerce/checkout-sdk'; +import { + BodlService, + CheckoutSelectors, + CheckoutService, + createCheckoutService, + createEmbeddedCheckoutMessenger, + EmbeddedCheckoutMessenger, + StepTracker, +} from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import { EventEmitter } from 'events'; import { noop, omit } from 'lodash'; @@ -12,24 +20,27 @@ import { getPhysicalItem } from '../cart/lineItem.mock'; import { createErrorLogger, CustomError, ErrorModal } from '../common/error'; import { getStoreConfig } from '../config/config.mock'; import { CustomerInfo, CustomerInfoProps, CustomerProps, CustomerViewType } from '../customer'; -import { getCustomer } from '../customer/customers.mock'; import Customer from '../customer/Customer'; -import { createEmbeddedCheckoutStylesheet, createEmbeddedCheckoutSupport } from '../embeddedCheckout'; +import { getCustomer } from '../customer/customers.mock'; +import { + createEmbeddedCheckoutStylesheet, + createEmbeddedCheckoutSupport, +} from '../embeddedCheckout'; import { getLanguageService, LocaleProvider } from '../locale'; import { PaymentProps } from '../payment'; import Payment from '../payment/Payment'; import { PromotionBannerList } from '../promotion'; import { ShippingProps, StaticConsignment } from '../shipping'; import { getConsignment } from '../shipping/consignment.mock'; -import { getShippingAddress } from '../shipping/shipping-addresses.mock'; import Shipping from '../shipping/Shipping'; +import { getShippingAddress } from '../shipping/shipping-addresses.mock'; -import { getCheckout, getCheckoutWithPromotions } from './checkouts.mock'; -import getCheckoutStepStatuses from './getCheckoutStepStatuses'; import Checkout, { CheckoutProps, WithCheckoutProps } from './Checkout'; import CheckoutProvider from './CheckoutProvider'; +import { getCheckout, getCheckoutWithPromotions } from './checkouts.mock'; import CheckoutStep, { CheckoutStepProps } from './CheckoutStep'; import CheckoutStepType from './CheckoutStepType'; +import getCheckoutStepStatuses from './getCheckoutStepStatuses'; describe('Checkout', () => { let CheckoutTest: FunctionComponent; @@ -44,7 +55,9 @@ describe('Checkout', () => { beforeEach(() => { checkoutService = createCheckoutService(); checkoutState = checkoutService.getState(); - embeddedMessengerMock = createEmbeddedCheckoutMessenger({ parentOrigin: getStoreConfig().links.siteLink }); + embeddedMessengerMock = createEmbeddedCheckoutMessenger({ + parentOrigin: getStoreConfig().links.siteLink, + }); stepTracker = { trackCheckoutStarted: jest.fn(), trackStepViewed: jest.fn(), @@ -66,10 +79,10 @@ describe('Checkout', () => { createBodlService: () => bodlService, }; - jest.spyOn(checkoutService, 'loadCheckout') - .mockImplementation(() => new Promise(resolve => { - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue({ + jest.spyOn(checkoutService, 'loadCheckout').mockImplementation( + () => + new Promise((resolve) => { + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue({ ...getStoreConfig(), checkoutSettings: { ...getStoreConfig().checkoutSettings, @@ -77,197 +90,189 @@ describe('Checkout', () => { }, }); - resolve(checkoutState); - })); + resolve(checkoutState); + }), + ); - jest.spyOn(checkoutService, 'getState') - .mockImplementation(() => checkoutState); + jest.spyOn(checkoutService, 'getState').mockImplementation(() => checkoutState); - jest.spyOn(checkoutService, 'subscribe') - .mockImplementation(subscriber => { - subscribeEventEmitter.on('change', () => subscriber(checkoutService.getState())); - subscribeEventEmitter.emit('change'); + jest.spyOn(checkoutService, 'subscribe').mockImplementation((subscriber) => { + subscribeEventEmitter.on('change', () => subscriber(checkoutService.getState())); + subscribeEventEmitter.emit('change'); - return noop; - }); + return noop; + }); - jest.spyOn(defaultProps.errorLogger, 'log') - .mockImplementation(noop); + jest.spyOn(defaultProps.errorLogger, 'log').mockImplementation(noop); - jest.spyOn(checkoutState.data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(checkoutState.data, 'getCart').mockReturnValue(getCart()); - jest.spyOn(checkoutState.data, 'getCheckout') - .mockReturnValue(getCheckout()); + jest.spyOn(checkoutState.data, 'getCheckout').mockReturnValue(getCheckout()); - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - CheckoutTest = props => ( - - - + CheckoutTest = (props) => ( + + + ); }); it('loads checkout when mounted', () => { - mount(); - - expect(checkoutService.loadCheckout) - .toHaveBeenCalledWith(defaultProps.checkoutId, { - params: { - include: [ - 'cart.lineItems.physicalItems.categoryNames', - 'cart.lineItems.digitalItems.categoryNames', - ], - }, - }); + mount(); + + expect(checkoutService.loadCheckout).toHaveBeenCalledWith(defaultProps.checkoutId, { + params: { + include: [ + 'cart.lineItems.physicalItems.categoryNames', + 'cart.lineItems.digitalItems.categoryNames', + ], + }, + }); }); it('tracks checkout started when config is ready', async () => { - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(undefined); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(undefined); - const component = mount(); + const component = mount(); component.setProps({ hasConfig: true }); component.update(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(stepTracker.trackCheckoutStarted) - .toHaveBeenCalled(); + expect(stepTracker.trackCheckoutStarted).toHaveBeenCalled(); }); it('tracks BODL checkout begin', async () => { - const container = mount(); + const container = mount(); + container.update(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(bodlService.checkoutBegin) - .toHaveBeenCalled(); + expect(bodlService.checkoutBegin).toHaveBeenCalled(); }); it('posts message to parent of embedded checkout when checkout is loaded', async () => { - jest.spyOn(embeddedMessengerMock, 'postFrameLoaded') - .mockImplementation(); + jest.spyOn(embeddedMessengerMock, 'postFrameLoaded').mockImplementation(); - mount(); + mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(embeddedMessengerMock.postFrameLoaded) - .toHaveBeenCalledWith({ contentId: defaultProps.containerId }); + expect(embeddedMessengerMock.postFrameLoaded).toHaveBeenCalledWith({ + contentId: defaultProps.containerId, + }); }); it('attaches additional styles for embedded checkout', async () => { const styles = { text: { color: '#000' } }; - jest.spyOn(embeddedMessengerMock, 'receiveStyles') - .mockImplementation(fn => fn(styles)); + jest.spyOn(embeddedMessengerMock, 'receiveStyles').mockImplementation((fn) => fn(styles)); - jest.spyOn(defaultProps.embeddedStylesheet, 'append') - .mockImplementation(); + jest.spyOn(defaultProps.embeddedStylesheet, 'append').mockImplementation(); - mount(); + mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(defaultProps.embeddedStylesheet.append) - .toHaveBeenCalledWith(styles); + expect(defaultProps.embeddedStylesheet.append).toHaveBeenCalledWith(styles); }); it('renders modal error when theres an error flash message', async () => { - jest.spyOn(checkoutState.data, 'getFlashMessages') - .mockReturnValue([ - { - message: 'flash message', - title: '', - type: 'error', - }, - ]); + jest.spyOn(checkoutState.data, 'getFlashMessages').mockReturnValue([ + { + message: 'flash message', + title: '', + type: 'error', + }, + ]); - const container = mount(); + const container = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); - expect(container.find(ErrorModal).prop('error')) - .toEqual(new Error('flash message')); + expect(container.find(ErrorModal).prop('error')).toEqual(new Error('flash message')); }); it('renders modal error when theres an custom error flash message', async () => { - jest.spyOn(checkoutState.data, 'getFlashMessages') - .mockReturnValue([ - { - message: 'flash message', - title: 'flash title', - type: 'error', - }, - ]); + jest.spyOn(checkoutState.data, 'getFlashMessages').mockReturnValue([ + { + message: 'flash message', + title: 'flash title', + type: 'error', + }, + ]); - const container = mount(); + const container = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); - const errorData = {data: {}, message: 'flash message', title: 'flash title', name: 'default'}; - expect(container.find(ErrorModal).prop('error')) - .toEqual(new CustomError(errorData)); + const errorData = { + data: {}, + message: 'flash message', + title: 'flash title', + name: 'default', + }; + + expect(container.find(ErrorModal).prop('error')).toEqual(new CustomError(errorData)); }); it('renders required checkout steps', () => { - const container = mount(); + const container = mount(); const steps = container.find(CheckoutStep); - expect(steps.at(0).prop('type')) - .toEqual(CheckoutStepType.Customer); + expect(steps.at(0).prop('type')).toEqual(CheckoutStepType.Customer); - expect(steps.at(1).prop('type')) - .toEqual(CheckoutStepType.Shipping); + expect(steps.at(1).prop('type')).toEqual(CheckoutStepType.Shipping); - expect(steps.at(2).prop('type')) - .toEqual(CheckoutStepType.Billing); + expect(steps.at(2).prop('type')).toEqual(CheckoutStepType.Billing); - expect(steps.at(3).prop('type')) - .toEqual(CheckoutStepType.Payment); + expect(steps.at(3).prop('type')).toEqual(CheckoutStepType.Payment); }); it('does not render checkout step if not required', () => { - jest.spyOn(checkoutState.data, 'getCart') - .mockReturnValue({ - ...getCart(), - lineItems: { - ...getCart().lineItems, - physicalItems: [], - }, - }); + jest.spyOn(checkoutState.data, 'getCart').mockReturnValue({ + ...getCart(), + lineItems: { + ...getCart().lineItems, + physicalItems: [], + }, + }); - const container = mount(); + const container = mount(); const steps = container.find(CheckoutStep); // When there's no physical item, shipping step shouldn't be rendered - expect(steps.findWhere(step => step.prop('type') === CheckoutStepType.Shipping)) - .toHaveLength(0); + expect( + steps.findWhere((step) => step.prop('type') === CheckoutStepType.Shipping), + ).toHaveLength(0); }); it('marks first incomplete step as active by default', async () => { - const container = mount(); + const container = mount(); // Wait for initial load to complete - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); const steps = container.find(CheckoutStep); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const activeStepType = getCheckoutStepStatuses(checkoutState) - .find(({ isActive }) => isActive === true)!.type; + const activeStepType = getCheckoutStepStatuses(checkoutState).find( + ({ isActive }) => isActive === true, + )!.type; - expect(steps.findWhere(step => step.prop('type') === activeStepType).at(0).prop('isActive')) - .toEqual(true); + expect( + steps + .findWhere((step) => step.prop('type') === activeStepType) + .at(0) + .prop('isActive'), + ).toBe(true); }); it('calls trackStepViewed when a step is expanded', async () => { @@ -275,181 +280,182 @@ describe('Checkout', () => { // JSDOM does not support `scrollTo` window.scrollTo = jest.fn(); - const container = mount(); + const container = mount(); - const step = container.find(CheckoutStep) - .findWhere(component => component.prop('type') === CheckoutStepType.Shipping) + const step = container + .find(CheckoutStep) + .findWhere((component) => component.prop('type') === CheckoutStepType.Shipping) .at(0); step.prop('onEdit')(CheckoutStepType.Shipping); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); jest.runAllTimers(); - expect(stepTracker.trackStepViewed) - .toHaveBeenCalledWith('shipping'); + expect(stepTracker.trackStepViewed).toHaveBeenCalledWith('shipping'); }); it('marks step as active when user tries to edit it', () => { - const container = mount(); + const container = mount(); - let step = container.find(CheckoutStep) - .findWhere(component => component.prop('type') === CheckoutStepType.Shipping) + let step = container + .find(CheckoutStep) + .findWhere((component) => component.prop('type') === CheckoutStepType.Shipping) .at(0); - expect(step.prop('isActive')) - .toEqual(false); + expect(step.prop('isActive')).toBe(false); // Trigger edit event on step step.prop('onEdit')(CheckoutStepType.Shipping); - step = container.update() + step = container + .update() .find(CheckoutStep) - .findWhere(component => component.prop('type') === CheckoutStepType.Shipping) + .findWhere((component) => component.prop('type') === CheckoutStepType.Shipping) .at(0); - expect(step.prop('isActive')) - .toEqual(true); + expect(step.prop('isActive')).toBe(true); }); it('renders list of promotion banners', () => { const checkout = getCheckoutWithPromotions(); - jest.spyOn(checkoutState.data, 'getCheckout') - .mockReturnValue(checkout); + jest.spyOn(checkoutState.data, 'getCheckout').mockReturnValue(checkout); - const container = mount(); + const container = mount(); - expect(container.find(PromotionBannerList)) - .toHaveLength(1); + expect(container.find(PromotionBannerList)).toHaveLength(1); - expect(container.find(PromotionBannerList).prop('promotions')) - .toEqual(checkout.promotions); + expect(container.find(PromotionBannerList).prop('promotions')).toEqual(checkout.promotions); }); describe('customer step', () => { let container: ReactWrapper; beforeEach(async () => { - container = mount(); + container = mount(); (container.find(CheckoutStep) as ReactWrapper) - .findWhere(step => step.prop('type') === CheckoutStepType.Customer) + .findWhere((step) => step.prop('type') === CheckoutStepType.Customer) .at(0) .prop('onEdit')(CheckoutStepType.Customer); // Wait for initial load to complete - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); }); it('renders customer component when customer step is active', () => { - expect(container.find(Customer).length) - .toEqual(1); + expect(container.find(Customer)).toHaveLength(1); }); it('calls trackStepComplete when switching steps', () => { container.setProps({ - steps: getCheckoutStepStatuses(checkoutState) - .map(step => ({ - ...step, - isActive: step.type === CheckoutStepType.Shipping ? true : false, - })), + steps: getCheckoutStepStatuses(checkoutState).map((step) => ({ + ...step, + isActive: step.type === CheckoutStepType.Shipping, + })), }); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (container.find(Customer).at(0) as ReactWrapper) - .prop('onSignIn')!(); + (container.find(Customer).at(0) as ReactWrapper).prop('onSignIn')!(); container.update(); - expect(stepTracker.trackStepCompleted) - .toHaveBeenCalledWith('customer'); + expect(stepTracker.trackStepCompleted).toHaveBeenCalledWith('customer'); }); it('navigates to next step when shopper signs in', () => { container.setProps({ - steps: getCheckoutStepStatuses(checkoutState) - .map(step => ({ - ...step, - isActive: step.type === CheckoutStepType.Shipping ? true : false, - })), + steps: getCheckoutStepStatuses(checkoutState).map((step) => ({ + ...step, + isActive: step.type === CheckoutStepType.Shipping, + })), }); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (container.find(Customer).at(0) as ReactWrapper) - .prop('onSignIn')!(); + (container.find(Customer).at(0) as ReactWrapper).prop('onSignIn')!(); container.update(); const steps: ReactWrapper = container.find(CheckoutStep); - expect(steps.findWhere(step => step.prop('type') === CheckoutStepType.Shipping).at(0).prop('isActive')) - .toEqual(true); + expect( + steps + .findWhere((step) => step.prop('type') === CheckoutStepType.Shipping) + .at(0) + .prop('isActive'), + ).toBe(true); }); it('navigates to next step when account is created', () => { container.setProps({ - steps: getCheckoutStepStatuses(checkoutState) - .map(step => ({ - ...step, - isActive: step.type === CheckoutStepType.Shipping ? true : false, - })), + steps: getCheckoutStepStatuses(checkoutState).map((step) => ({ + ...step, + isActive: step.type === CheckoutStepType.Shipping, + })), }); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (container.find(Customer).at(0) as ReactWrapper) - .prop('onAccountCreated')!(); + (container.find(Customer).at(0) as ReactWrapper).prop( + 'onAccountCreated', + )!(); container.update(); const steps: ReactWrapper = container.find(CheckoutStep); - expect(steps.findWhere(step => step.prop('type') === CheckoutStepType.Shipping).at(0).prop('isActive')) - .toEqual(true); + expect( + steps + .findWhere((step) => step.prop('type') === CheckoutStepType.Shipping) + .at(0) + .prop('isActive'), + ).toBe(true); }); it('navigates to next step when shopper continues as guest', () => { container.setProps({ - steps: getCheckoutStepStatuses(checkoutState) - .map(step => ({ - ...step, - isActive: step.type === CheckoutStepType.Shipping ? true : false, - })), + steps: getCheckoutStepStatuses(checkoutState).map((step) => ({ + ...step, + isActive: step.type === CheckoutStepType.Shipping, + })), }); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (container.find(Customer).at(0) as ReactWrapper) - .prop('onContinueAsGuest')!(); + (container.find(Customer).at(0) as ReactWrapper).prop( + 'onContinueAsGuest', + )!(); container.update(); const steps: ReactWrapper = container.find(CheckoutStep); - expect(steps.findWhere(step => step.prop('type') === CheckoutStepType.Shipping).at(0).prop('isActive')) - .toEqual(true); + expect( + steps + .findWhere((step) => step.prop('type') === CheckoutStepType.Shipping) + .at(0) + .prop('isActive'), + ).toBe(true); }); it('renders guest form after sign out', () => { checkoutState = { ...checkoutState }; - jest.spyOn(checkoutState.data, 'getCustomer') - .mockReturnValue(getCustomer()); + jest.spyOn(checkoutState.data, 'getCustomer').mockReturnValue(getCustomer()); - container = mount(); + container = mount(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (container.find(CustomerInfo) as ReactWrapper) - .prop('onSignOut')!({ isCartEmpty: false }); + (container.find(CustomerInfo) as ReactWrapper).prop('onSignOut')!({ + isCartEmpty: false, + }); - jest.spyOn(checkoutState.data, 'getCustomer') - .mockReturnValue(undefined); + jest.spyOn(checkoutState.data, 'getCustomer').mockReturnValue(undefined); container.update(); - expect(container.find(Customer).prop('viewType')) - .toEqual(CustomerViewType.Guest); + expect(container.find(Customer).prop('viewType')).toEqual(CustomerViewType.Guest); }); it('navigates to login page if cart is empty after sign out', () => { @@ -464,17 +470,16 @@ describe('Checkout', () => { writable: true, }); - jest.spyOn(checkoutState.data, 'getCustomer') - .mockReturnValue(getCustomer()); + jest.spyOn(checkoutState.data, 'getCustomer').mockReturnValue(getCustomer()); - container = mount(); + container = mount(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (container.find(CustomerInfo) as ReactWrapper) - .prop('onSignOut')!({ isCartEmpty: true }); + (container.find(CustomerInfo) as ReactWrapper).prop('onSignOut')!({ + isCartEmpty: true, + }); - expect(window.top?.location.assign) - .toHaveBeenCalled(); + expect(window.top?.location.assign).toHaveBeenCalled(); }); it('navigates to cart page after sign out if prices are restricted to login', () => { @@ -489,60 +494,58 @@ describe('Checkout', () => { writable: true, }); - jest.spyOn(checkoutState.data, 'getCustomer') - .mockReturnValue(getCustomer()); + jest.spyOn(checkoutState.data, 'getCustomer').mockReturnValue(getCustomer()); - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue({ - ...getStoreConfig(), - displaySettings: { - hidePriceFromGuests: true, - }, - }); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue({ + ...getStoreConfig(), + displaySettings: { + hidePriceFromGuests: true, + }, + }); - container = mount(); + container = mount(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (container.find(CustomerInfo) as ReactWrapper) - .prop('onSignOut')!({ isCartEmpty: false }); - + (container.find(CustomerInfo) as ReactWrapper).prop('onSignOut')!({ + isCartEmpty: false, + }); + const cartUrl = getStoreConfig().links.cartLink; - expect(window.top?.location.href) - .toEqual(cartUrl); + expect(window.top?.location.href).toEqual(cartUrl); }); it('logs unhandled error', () => { const error = new Error(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (container.find(Customer).at(0) as ReactWrapper) - .prop('onUnhandledError')!(error); + (container.find(Customer).at(0) as ReactWrapper).prop( + 'onUnhandledError', + )!(error); - expect(defaultProps.errorLogger.log) - .toHaveBeenCalledWith(error); + expect(defaultProps.errorLogger.log).toHaveBeenCalledWith(error); }); it('logs error if shopper is unable to sign in', () => { const error = new Error(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (container.find(Customer).at(0) as ReactWrapper) - .prop('onSignInError')!(error); + (container.find(Customer).at(0) as ReactWrapper).prop('onSignInError')!( + error, + ); - expect(defaultProps.errorLogger.log) - .toHaveBeenCalledWith(error); + expect(defaultProps.errorLogger.log).toHaveBeenCalledWith(error); }); it('logs error if shopper is unable to continue as guest', () => { const error = new Error(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (container.find(Customer).at(0) as ReactWrapper) - .prop('onContinueAsGuestError')!(error); + (container.find(Customer).at(0) as ReactWrapper).prop( + 'onContinueAsGuestError', + )!(error); - expect(defaultProps.errorLogger.log) - .toHaveBeenCalledWith(error); + expect(defaultProps.errorLogger.log).toHaveBeenCalledWith(error); }); }); @@ -550,56 +553,52 @@ describe('Checkout', () => { let container: ReactWrapper; beforeEach(async () => { - container = mount(); + container = mount(); (container.find(CheckoutStep) as ReactWrapper) - .findWhere(step => step.prop('type') === CheckoutStepType.Shipping) + .findWhere((step) => step.prop('type') === CheckoutStepType.Shipping) .at(0) .prop('onEdit')(CheckoutStepType.Shipping); // Wait for initial load to complete - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); }); it('renders shipping component when shipping step is active', () => { - expect(container.find(Shipping).length) - .toEqual(1); + expect(container.find(Shipping)).toHaveLength(1); }); it('renders multi-shipping when enabled and there are multiple consignments', async () => { - jest.spyOn(checkoutState.data, 'getConsignments') - .mockReturnValue([ - omit(getConsignment(), 'selectedShippingOption'), - omit(getConsignment(), 'selectedShippingOption'), - ]); + jest.spyOn(checkoutState.data, 'getConsignments').mockReturnValue([ + omit(getConsignment(), 'selectedShippingOption'), + omit(getConsignment(), 'selectedShippingOption'), + ]); - container = mount(); + container = mount(); (container.find(CheckoutStep) as ReactWrapper) - .findWhere(step => step.prop('type') === CheckoutStepType.Shipping) + .findWhere((step) => step.prop('type') === CheckoutStepType.Shipping) .at(0) .prop('onEdit')(CheckoutStepType.Shipping); // Wait for initial load to complete - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); - expect(container.find(Shipping).at(0).prop('isMultiShippingMode')) - .toEqual(true); + expect(container.find(Shipping).at(0).prop('isMultiShippingMode')).toBe(true); }); it('does not render multi-shipping when disabled even if there are multiple consignments', async () => { - jest.spyOn(checkoutState.data, 'getConsignments') - .mockReturnValue([ - omit(getConsignment(), 'selectedShippingOption'), - omit(getConsignment(), 'selectedShippingOption'), - ]); - - jest.spyOn(checkoutService, 'loadCheckout') - .mockImplementation(() => new Promise(resolve => { - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue({ + jest.spyOn(checkoutState.data, 'getConsignments').mockReturnValue([ + omit(getConsignment(), 'selectedShippingOption'), + omit(getConsignment(), 'selectedShippingOption'), + ]); + + jest.spyOn(checkoutService, 'loadCheckout').mockImplementation( + () => + new Promise((resolve) => { + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue({ ...getStoreConfig(), checkoutSettings: { ...getStoreConfig().checkoutSettings, @@ -607,22 +606,22 @@ describe('Checkout', () => { }, }); - resolve(checkoutState); - })); + resolve(checkoutState); + }), + ); - container = mount(); + container = mount(); (container.find(CheckoutStep) as ReactWrapper) - .findWhere(step => step.prop('type') === CheckoutStepType.Shipping) + .findWhere((step) => step.prop('type') === CheckoutStepType.Shipping) .at(0) .prop('onEdit')(CheckoutStepType.Shipping); // Wait for initial load to complete - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); - expect(container.find(Shipping).at(0).prop('isMultiShippingMode')) - .toEqual(false); + expect(container.find(Shipping).at(0).prop('isMultiShippingMode')).toBe(false); }); it('navigates to login view when shopper tries to sign in in order to use multi-shipping feature', () => { @@ -632,14 +631,14 @@ describe('Checkout', () => { container.update(); const steps: ReactWrapper = container.find(CheckoutStep); - const customerStep: ReactWrapper = steps.findWhere(step => step.prop('type') === CheckoutStepType.Customer).at(0); + const customerStep: ReactWrapper = steps + .findWhere((step) => step.prop('type') === CheckoutStepType.Customer) + .at(0); const customer: ReactWrapper = container.find(Customer).at(0); - expect(customerStep.prop('isActive')) - .toEqual(true); + expect(customerStep.prop('isActive')).toBe(true); - expect(customer.prop('viewType')) - .toEqual(CustomerViewType.Login); + expect(customer.prop('viewType')).toEqual(CustomerViewType.Login); }); it('navigates to billing step if not using shipping address as billing address', () => { @@ -649,10 +648,11 @@ describe('Checkout', () => { container.update(); const steps: ReactWrapper = container.find(CheckoutStep); - const nextStep: ReactWrapper = steps.findWhere(step => step.prop('type') === CheckoutStepType.Billing).at(0); + const nextStep: ReactWrapper = steps + .findWhere((step) => step.prop('type') === CheckoutStepType.Billing) + .at(0); - expect(nextStep.prop('isActive')) - .toEqual(true); + expect(nextStep.prop('isActive')).toBe(true); }); it('navigates to next incomplete step if using shipping address as billing address', () => { @@ -662,23 +662,25 @@ describe('Checkout', () => { container.update(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const activeStepType = getCheckoutStepStatuses(checkoutState) - .find(({ isActive }) => isActive === true)!.type; + const activeStepType = getCheckoutStepStatuses(checkoutState).find( + ({ isActive }) => isActive === true, + )!.type; const steps: ReactWrapper = container.find(CheckoutStep); - const nextStep: ReactWrapper = steps.findWhere(step => step.prop('type') === activeStepType).at(0); + const nextStep: ReactWrapper = steps + .findWhere((step) => step.prop('type') === activeStepType) + .at(0); - expect(nextStep.prop('isActive')) - .toEqual(true); + expect(nextStep.prop('isActive')).toBe(true); }); it('logs unhandled error', () => { const error = new Error(); - (container.find(Shipping).at(0) as ReactWrapper) - .prop('onUnhandledError')(error); + (container.find(Shipping).at(0) as ReactWrapper).prop( + 'onUnhandledError', + )(error); - expect(defaultProps.errorLogger.log) - .toHaveBeenCalledWith(error); + expect(defaultProps.errorLogger.log).toHaveBeenCalledWith(error); }); }); @@ -690,48 +692,46 @@ describe('Checkout', () => { }; beforeEach(async () => { - jest.spyOn(checkoutState.data, 'getConsignments') - .mockReturnValue([consignment]); + jest.spyOn(checkoutState.data, 'getConsignments').mockReturnValue([consignment]); - jest.spyOn(checkoutState.data, 'getShippingAddress') - .mockReturnValue(getShippingAddress()); + jest.spyOn(checkoutState.data, 'getShippingAddress').mockReturnValue( + getShippingAddress(), + ); - jest.spyOn(checkoutState.data, 'getCustomer') - .mockReturnValue(getCustomer()); + jest.spyOn(checkoutState.data, 'getCustomer').mockReturnValue(getCustomer()); - container = mount(); + container = mount(); // Wait for initial load to complete - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); }); it('renders shipping component with summary data', () => { - expect((container.find(CheckoutStep) as ReactWrapper) - .at(1) - .find(StaticConsignment) - .props() - ) - .toMatchObject({ - cart: getCart(), - compactView: true, - consignment, - }); + expect( + (container.find(CheckoutStep) as ReactWrapper) + .at(1) + .find(StaticConsignment) + .props(), + ).toMatchObject({ + cart: getCart(), + compactView: true, + consignment, + }); }); it('renders billing component when billing step is active', () => { - expect(container.find(Billing).length) - .toEqual(1); + expect(container.find(Billing)).toHaveLength(1); }); it('logs unhandled error', () => { const error = new Error(); - (container.find(Billing).at(0) as ReactWrapper) - .prop('onUnhandledError')(error); + (container.find(Billing).at(0) as ReactWrapper).prop('onUnhandledError')( + error, + ); - expect(defaultProps.errorLogger.log) - .toHaveBeenCalledWith(error); + expect(defaultProps.errorLogger.log).toHaveBeenCalledWith(error); }); }); @@ -747,101 +747,95 @@ describe('Checkout', () => { writable: true, }); - container = mount(); + container = mount(); (container.find(CheckoutStep) as ReactWrapper) - .findWhere(step => step.prop('type') === CheckoutStepType.Payment) + .findWhere((step) => step.prop('type') === CheckoutStepType.Payment) .at(0) .prop('onEdit')(CheckoutStepType.Payment); // Wait for initial load to complete - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - act(() => { container.update(); }); - act(() => { container.update(); }); + act(() => { + container.update(); + }); + act(() => { + container.update(); + }); }); it('renders payment component when payment step is active', () => { - expect(container.find(Payment).length) - .toEqual(1); + expect(container.find(Payment)).toHaveLength(1); }); it('navigates to order confirmation page when payment is submitted', () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (container.find(Payment).at(0) as ReactWrapper) - .prop('onSubmit')!(); + (container.find(Payment).at(0) as ReactWrapper).prop('onSubmit')!(); - expect(window.location.replace) - .toHaveBeenCalledWith('/checkout/order-confirmation'); + expect(window.location.replace).toHaveBeenCalledWith('/checkout/order-confirmation'); }); it('navigates to order confirmation page when order is finalized', () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (container.find(Payment).at(0) as ReactWrapper) - .prop('onFinalize')!(); + (container.find(Payment).at(0) as ReactWrapper).prop('onFinalize')!(); - expect(window.location.replace) - .toHaveBeenCalledWith('/checkout/order-confirmation'); + expect(window.location.replace).toHaveBeenCalledWith('/checkout/order-confirmation'); }); it('posts message to parent of embedded checkout when shopper completes checkout', () => { - jest.spyOn(embeddedMessengerMock, 'postComplete') - .mockImplementation(noop); + jest.spyOn(embeddedMessengerMock, 'postComplete').mockImplementation(noop); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (container.find(Payment).at(0) as ReactWrapper) - .prop('onSubmit')!(); + (container.find(Payment).at(0) as ReactWrapper).prop('onSubmit')!(); - expect(embeddedMessengerMock.postComplete) - .toHaveBeenCalled(); + expect(embeddedMessengerMock.postComplete).toHaveBeenCalled(); }); it('posts message to parent of embedded checkout when there is unhandled error', () => { - jest.spyOn(embeddedMessengerMock, 'postError') - .mockImplementation(noop); + jest.spyOn(embeddedMessengerMock, 'postError').mockImplementation(noop); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (container.find(Payment).at(0) as ReactWrapper) - .prop('onUnhandledError')!(new Error()); + (container.find(Payment).at(0) as ReactWrapper).prop('onUnhandledError')!( + new Error(), + ); - expect(embeddedMessengerMock.postError) - .toHaveBeenCalled(); + expect(embeddedMessengerMock.postError).toHaveBeenCalled(); }); it('logs unhandled error', () => { const error = new Error(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (container.find(Payment).at(0) as ReactWrapper) - .prop('onUnhandledError')!(error); + (container.find(Payment).at(0) as ReactWrapper).prop('onUnhandledError')!( + error, + ); - expect(defaultProps.errorLogger.log) - .toHaveBeenCalledWith(error); + expect(defaultProps.errorLogger.log).toHaveBeenCalledWith(error); }); it('posts message to parent of embedded checkout when there is order submission error', () => { const error = new Error(); - jest.spyOn(embeddedMessengerMock, 'postError') - .mockImplementation(noop); + jest.spyOn(embeddedMessengerMock, 'postError').mockImplementation(noop); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (container.find(Payment).at(0) as ReactWrapper) - .prop('onSubmitError')!(error); + (container.find(Payment).at(0) as ReactWrapper).prop('onSubmitError')!( + error, + ); - expect(embeddedMessengerMock.postError) - .toHaveBeenCalledWith(error); + expect(embeddedMessengerMock.postError).toHaveBeenCalledWith(error); }); it('logs error if unable to submit order', () => { const error = new Error(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (container.find(Payment).at(0) as ReactWrapper) - .prop('onSubmitError')!(error); + (container.find(Payment).at(0) as ReactWrapper).prop('onSubmitError')!( + error, + ); - expect(defaultProps.errorLogger.log) - .toHaveBeenCalledWith(error); + expect(defaultProps.errorLogger.log).toHaveBeenCalledWith(error); }); }); }); diff --git a/packages/core/src/app/checkout/Checkout.tsx b/packages/core/src/app/checkout/Checkout.tsx index dd52e75915..d9aeb1c4ed 100644 --- a/packages/core/src/app/checkout/Checkout.tsx +++ b/packages/core/src/app/checkout/Checkout.tsx @@ -1,58 +1,107 @@ -import { Address, Cart, CartChangedError, CheckoutParams, CheckoutSelectors, Consignment, EmbeddedCheckoutMessenger, EmbeddedCheckoutMessengerOptions, FlashMessage, Promotion, RequestOptions, StepTracker, BodlService } from '@bigcommerce/checkout-sdk'; +import { + Address, + BodlService, + Cart, + CartChangedError, + CheckoutParams, + CheckoutSelectors, + Consignment, + EmbeddedCheckoutMessenger, + EmbeddedCheckoutMessengerOptions, + FlashMessage, + Promotion, + RequestOptions, + StepTracker, +} from '@bigcommerce/checkout-sdk'; import classNames from 'classnames'; import { find, findIndex } from 'lodash'; -import React, { lazy, Component, ReactNode } from 'react'; +import React, { Component, lazy, ReactNode } from 'react'; import { StaticBillingAddress } from '../billing'; import { EmptyCartMessage } from '../cart'; -import { isCustomError, CustomError, ErrorLogger, ErrorModal } from '../common/error'; +import { CustomError, ErrorLogger, ErrorModal, isCustomError } from '../common/error'; import { retry } from '../common/utility'; -import { CheckoutSuggestion, CustomerInfo, CustomerSignOutEvent, CustomerViewType } from '../customer'; -import { isEmbedded, EmbeddedCheckoutStylesheet } from '../embeddedCheckout'; -import { withLanguage, TranslatedString, WithLanguageProps } from '../locale'; +import { + CheckoutSuggestion, + CustomerInfo, + CustomerSignOutEvent, + CustomerViewType, +} from '../customer'; +import { EmbeddedCheckoutStylesheet, isEmbedded } from '../embeddedCheckout'; +import { TranslatedString, withLanguage, WithLanguageProps } from '../locale'; import { PromotionBannerList } from '../promotion'; import { hasSelectedShippingOptions, isUsingMultiShipping, StaticConsignment } from '../shipping'; import { ShippingOptionExpiredError } from '../shipping/shippingOption'; import { LazyContainer, LoadingNotification, LoadingOverlay } from '../ui/loading'; import { MobileView } from '../ui/responsive'; -import mapToCheckoutProps from './mapToCheckoutProps'; -import navigateToOrderConfirmation from './navigateToOrderConfirmation'; -import withCheckout from './withCheckout'; import CheckoutStep from './CheckoutStep'; import CheckoutStepStatus from './CheckoutStepStatus'; import CheckoutStepType from './CheckoutStepType'; import CheckoutSupport from './CheckoutSupport'; +import mapToCheckoutProps from './mapToCheckoutProps'; +import navigateToOrderConfirmation from './navigateToOrderConfirmation'; +import withCheckout from './withCheckout'; -const Billing = lazy(() => retry(() => import( - /* webpackChunkName: "billing" */ - '../billing/Billing' -))); - -const CartSummary = lazy(() => retry(() => import( - /* webpackChunkName: "cart-summary" */ - '../cart/CartSummary' -))); - -const CartSummaryDrawer = lazy(() => retry(() => import( - /* webpackChunkName: "cart-summary-drawer" */ - '../cart/CartSummaryDrawer' -))); - -const Customer = lazy(() => retry(() => import( - /* webpackChunkName: "customer" */ - '../customer/Customer' -))); - -const Payment = lazy(() => retry(() => import( - /* webpackChunkName: "payment" */ - '../payment/Payment' -))); - -const Shipping = lazy(() => retry(() => import( - /* webpackChunkName: "shipping" */ - '../shipping/Shipping' -))); +const Billing = lazy(() => + retry( + () => + import( + /* webpackChunkName: "billing" */ + '../billing/Billing' + ), + ), +); + +const CartSummary = lazy(() => + retry( + () => + import( + /* webpackChunkName: "cart-summary" */ + '../cart/CartSummary' + ), + ), +); + +const CartSummaryDrawer = lazy(() => + retry( + () => + import( + /* webpackChunkName: "cart-summary-drawer" */ + '../cart/CartSummaryDrawer' + ), + ), +); + +const Customer = lazy(() => + retry( + () => + import( + /* webpackChunkName: "customer" */ + '../customer/Customer' + ), + ), +); + +const Payment = lazy(() => + retry( + () => + import( + /* webpackChunkName: "payment" */ + '../payment/Payment' + ), + ), +); + +const Shipping = lazy(() => + retry( + () => + import( + /* webpackChunkName: "shipping" */ + '../shipping/Shipping' + ), + ), +); export interface CheckoutProps { checkoutId: string; @@ -101,7 +150,10 @@ export interface WithCheckoutProps { subscribeToConsignments(subscriber: (state: CheckoutSelectors) => void): () => void; } -class Checkout extends Component { +class Checkout extends Component< + CheckoutProps & WithCheckoutProps & WithLanguageProps, + CheckoutState +> { stepTracker: StepTracker | undefined; bodlService: BodlService | undefined; @@ -153,7 +205,9 @@ class Checkout extends Component embeddedStylesheet.append(styles)); + messenger.receiveStyles((styles) => embeddedStylesheet.append(styles)); messenger.postFrameLoaded({ contentId: containerId }); messenger.postLoaded(); @@ -178,15 +234,23 @@ class Checkout extends Component; + errorModal = ( + + ); } else { - errorModal = ; + errorModal = ; } } - return <> -
    + return ( +
    - { this.renderContent() } + {this.renderContent()}
    - { errorModal } + {errorModal}
    - - ; + ); } private renderContent(): ReactNode { - const { - isPending, - loginUrl, - promotions = [], - steps, - } = this.props; + const { isPending, loginUrl, promotions = [], steps } = this.props; - const { - activeStepType, - defaultStepType, - isCartEmpty, - isRedirecting, - } = this.state; + const { activeStepType, defaultStepType, isCartEmpty, isRedirecting } = this.state; if (isCartEmpty) { - return ( - - ); + return ; } return ( - +
    - + - +
      - { steps - .filter(step => step.isRequired) - .map(step => this.renderStep({ - ...step, - isActive: activeStepType ? activeStepType === step.type : defaultStepType === step.type, - isBusy: isPending, - })) } + {steps + .filter((step) => step.isRequired) + .map((step) => + this.renderStep({ + ...step, + isActive: activeStepType + ? activeStepType === step.type + : defaultStepType === step.type, + isBusy: isPending, + }), + )}
    - { this.renderCartSummary() } + {this.renderCartSummary()}
    ); } private renderStep(step: CheckoutStepStatus): ReactNode { switch (step.type) { - case CheckoutStepType.Customer: - return this.renderCustomerStep(step); + case CheckoutStepType.Customer: + return this.renderCustomerStep(step); - case CheckoutStepType.Shipping: - return this.renderShippingStep(step); + case CheckoutStepType.Shipping: + return this.renderShippingStep(step); - case CheckoutStepType.Billing: - return this.renderBillingStep(step); + case CheckoutStepType.Billing: + return this.renderBillingStep(step); - case CheckoutStepType.Payment: - return this.renderPaymentStep(step); + case CheckoutStepType.Payment: + return this.renderPaymentStep(step); - default: - return null; + default: + return null; } } @@ -301,32 +356,32 @@ class Checkout extends Component } - key={ step.type } - onEdit={ this.handleEditStep } - onExpanded={ this.handleExpanded } - suggestion={ } + {...step} + heading={} + key={step.type} + onEdit={this.handleEditStep} + onExpanded={this.handleExpanded} + suggestion={} summary={ } > @@ -334,16 +389,9 @@ class Checkout extends Component } - key={ step.type } - onEdit={ this.handleEditStep } - onExpanded={ this.handleExpanded } - summary={ consignments.map(consignment => -
    + {...step} + heading={} + key={step.type} + onEdit={this.handleEditStep} + onExpanded={this.handleExpanded} + summary={consignments.map((consignment) => ( +
    -
    ) } +
    + ))} > @@ -387,18 +436,18 @@ class Checkout extends Component } - key={ step.type } - onEdit={ this.handleEditStep } - onExpanded={ this.handleExpanded } - summary={ billingAddress && } + {...step} + heading={} + key={step.type} + onEdit={this.handleEditStep} + onExpanded={this.handleExpanded} + summary={billingAddress && } > @@ -406,32 +455,32 @@ class Checkout extends Component } - key={ step.type } - onEdit={ this.handleEditStep } - onExpanded={ this.handleExpanded } + {...step} + heading={} + key={step.type} + onEdit={this.handleEditStep} + onExpanded={this.handleExpanded} > @@ -441,19 +490,23 @@ class Checkout extends Component - { matched => { + {(matched) => { if (matched) { - return - - ; + return ( + + + + ); } - return ; - } } + return ( + + ); + }} ); } @@ -488,7 +541,9 @@ class Checkout extends Component void = options => { + private navigateToNextIncompleteStep: (options?: { isDefault?: boolean }) => void = ( + options, + ) => { const { steps } = this.props; const activeStepIndex = findIndex(steps, { isActive: true }); const activeStep = activeStepIndex >= 0 && steps[activeStepIndex]; @@ -506,7 +561,7 @@ class Checkout extends Component void = orderId => { + private navigateToOrderConfirmation: (orderId?: number) => void = (orderId) => { const { steps } = this.props; const { isBuyNowCartEnabled } = this.state; @@ -523,7 +578,7 @@ class Checkout extends Component boolean = methodIds => { + private checkEmbeddedSupport: (methodIds: string[]) => boolean = (methodIds) => { const { embeddedSupport } = this.props; return embeddedSupport.isSupported(...methodIds); @@ -534,18 +589,20 @@ class Checkout extends Component void = ({ data }) => { - const { - hasSelectedShippingOptions: prevHasSelectedShippingOptions, - activeStepType, - } = this.state; + const { hasSelectedShippingOptions: prevHasSelectedShippingOptions, activeStepType } = + this.state; const { steps } = this.props; - const newHasSelectedShippingOptions = hasSelectedShippingOptions(data.getConsignments() || []); + const newHasSelectedShippingOptions = hasSelectedShippingOptions( + data.getConsignments() || [], + ); - if (prevHasSelectedShippingOptions && + if ( + prevHasSelectedShippingOptions && !newHasSelectedShippingOptions && - findIndex(steps, { type: CheckoutStepType.Shipping }) < findIndex(steps, { type: activeStepType }) + findIndex(steps, { type: CheckoutStepType.Shipping }) < + findIndex(steps, { type: activeStepType }) ) { this.navigateToStep(CheckoutStepType.Shipping); this.setState({ error: new ShippingOptionExpiredError() }); @@ -558,13 +615,13 @@ class Checkout extends Component void = type => { + private handleExpanded: (type: CheckoutStepType) => void = (type) => { if (this.stepTracker) { - this.stepTracker.trackStepViewed(type); + this.stepTracker.trackStepViewed(type); } }; - private handleUnhandledError: (error: Error) => void = error => { + private handleUnhandledError: (error: Error) => void = (error) => { this.handleError(error); // For errors that are not caught and handled by child components, we @@ -572,7 +629,7 @@ class Checkout extends Component void = error => { + private handleError: (error: Error) => void = (error) => { const { errorLogger } = this.props; errorLogger.log(error); @@ -582,7 +639,7 @@ class Checkout extends Component void = type => { + private handleEditStep: (type: CheckoutStepType) => void = (type) => { this.navigateToStep(type); }; @@ -595,7 +652,7 @@ class Checkout extends Component void = isBillingSameAsShipping => { + private handleShippingNextStep: (isBillingSameAsShipping: boolean) => void = ( + isBillingSameAsShipping, + ) => { this.setState({ isBillingSameAsShipping }); if (isBillingSameAsShipping) { @@ -638,13 +697,11 @@ class Checkout extends Component void = customerViewType => { - const { - canCreateAccountInCheckout, - createAccountUrl, - } = this.props; + private setCustomerViewType: (viewType: CustomerViewType) => void = (customerViewType) => { + const { canCreateAccountInCheckout, createAccountUrl } = this.props; - if (customerViewType === CustomerViewType.CreateAccount && + if ( + customerViewType === CustomerViewType.CreateAccount && (!canCreateAccountInCheckout || isEmbedded()) ) { if (window.top) { diff --git a/packages/core/src/app/checkout/CheckoutApp.spec.tsx b/packages/core/src/app/checkout/CheckoutApp.spec.tsx index 842e05c70d..044852df53 100644 --- a/packages/core/src/app/checkout/CheckoutApp.spec.tsx +++ b/packages/core/src/app/checkout/CheckoutApp.spec.tsx @@ -1,10 +1,10 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { getCheckout } from './checkouts.mock'; import Checkout from './Checkout'; import CheckoutApp, { CheckoutAppProps } from './CheckoutApp'; import CheckoutProvider from './CheckoutProvider'; +import { getCheckout } from './checkouts.mock'; describe('CheckoutApp', () => { let defaultProps: CheckoutAppProps; @@ -26,16 +26,14 @@ describe('CheckoutApp', () => { }); it('renders checkout component', () => { - const component = shallow(); + const component = shallow(); - expect(component.find(Checkout)) - .toHaveLength(1); + expect(component.find(Checkout)).toHaveLength(1); }); it('provides checkout context', () => { - const component = shallow(); + const component = shallow(); - expect(component.find(CheckoutProvider)) - .toHaveLength(1); + expect(component.find(CheckoutProvider)).toHaveLength(1); }); }); diff --git a/packages/core/src/app/checkout/CheckoutApp.tsx b/packages/core/src/app/checkout/CheckoutApp.tsx index 6de7adabf4..0b215bfe3f 100644 --- a/packages/core/src/app/checkout/CheckoutApp.tsx +++ b/packages/core/src/app/checkout/CheckoutApp.tsx @@ -1,11 +1,21 @@ -import { createCheckoutService, createEmbeddedCheckoutMessenger, createStepTracker, StepTracker, createBodlService, BodlService } from '@bigcommerce/checkout-sdk'; +import { + BodlService, + createBodlService, + createCheckoutService, + createEmbeddedCheckoutMessenger, + createStepTracker, + StepTracker, +} from '@bigcommerce/checkout-sdk'; import { BrowserOptions } from '@sentry/browser'; import React, { Component } from 'react'; import ReactModal from 'react-modal'; import '../../scss/App.scss'; import { createErrorLogger, ErrorBoundary, ErrorLogger } from '../common/error'; -import { createEmbeddedCheckoutStylesheet, createEmbeddedCheckoutSupport } from '../embeddedCheckout'; +import { + createEmbeddedCheckoutStylesheet, + createEmbeddedCheckoutSupport, +} from '../embeddedCheckout'; import { getLanguageService, LocaleProvider } from '../locale'; import Checkout from './Checkout'; @@ -35,7 +45,7 @@ export default class CheckoutApp extends Component { { errorTypes: ['UnrecoverableError'], publicPath: props.publicPath, - } + }, ); } @@ -47,17 +57,17 @@ export default class CheckoutApp extends Component { render() { return ( - - - + + + diff --git a/packages/core/src/app/checkout/CheckoutProvider.spec.tsx b/packages/core/src/app/checkout/CheckoutProvider.spec.tsx index 1cce1a6a1a..fb9996d155 100644 --- a/packages/core/src/app/checkout/CheckoutProvider.spec.tsx +++ b/packages/core/src/app/checkout/CheckoutProvider.spec.tsx @@ -10,27 +10,23 @@ describe('CheckoutProvider', () => { jest.spyOn(service, 'subscribe'); - const component = mount(); + const component = mount(); - expect(service.subscribe) - .toHaveBeenCalled(); + expect(service.subscribe).toHaveBeenCalled(); - expect(component.state('checkoutState')) - .toEqual(service.getState()); + expect(component.state('checkoutState')).toEqual(service.getState()); }); it('unsubscribes to state changes when component unmounts', () => { const service = createCheckoutService(); const unsubscribe = jest.fn(); - jest.spyOn(service, 'subscribe') - .mockReturnValue(unsubscribe); + jest.spyOn(service, 'subscribe').mockReturnValue(unsubscribe); - const component = mount(); + const component = mount(); component.unmount(); - expect(unsubscribe) - .toHaveBeenCalled(); + expect(unsubscribe).toHaveBeenCalled(); }); }); diff --git a/packages/core/src/app/checkout/CheckoutProvider.tsx b/packages/core/src/app/checkout/CheckoutProvider.tsx index bf15264b6b..8d6d3911e2 100644 --- a/packages/core/src/app/checkout/CheckoutProvider.tsx +++ b/packages/core/src/app/checkout/CheckoutProvider.tsx @@ -12,7 +12,10 @@ export interface CheckoutProviderState { checkoutState: CheckoutSelectors; } -export default class CheckoutProvider extends Component { +export default class CheckoutProvider extends Component< + CheckoutProviderProps, + CheckoutProviderState +> { state: Readonly; private unsubscribe?: () => void; @@ -35,8 +38,8 @@ export default class CheckoutProvider extends Component - this.setState({ checkoutState }) + this.unsubscribe = checkoutService.subscribe((checkoutState) => + this.setState({ checkoutState }), ); } @@ -52,8 +55,8 @@ export default class CheckoutProvider extends Component - { children } + + {children} ); } diff --git a/packages/core/src/app/checkout/CheckoutStep.spec.tsx b/packages/core/src/app/checkout/CheckoutStep.spec.tsx index 3f3fa5f636..cbc8c53aa4 100644 --- a/packages/core/src/app/checkout/CheckoutStep.spec.tsx +++ b/packages/core/src/app/checkout/CheckoutStep.spec.tsx @@ -36,13 +36,19 @@ describe('CheckoutStep', () => { window.scrollTo = jest.fn(); // Mock `matchMedia` to detect mobile viewport - window.matchMedia = jest.fn(query => ({ - matches: query === `screen and (max-width: ${MOBILE_MAX_WIDTH}px)` ? isMobile : false, - addListener: noop, - addEventListener: noop, - removeListener: noop, - removeEventListener: noop, - }) as MediaQueryList); + window.matchMedia = jest.fn( + (query) => + ({ + matches: + query === `screen and (max-width: ${MOBILE_MAX_WIDTH}px)` + ? isMobile + : false, + addListener: noop, + addEventListener: noop, + removeListener: noop, + removeEventListener: noop, + } as MediaQueryList), + ); }); afterEach(() => { @@ -57,70 +63,63 @@ describe('CheckoutStep', () => { it('focuses on first form input when step is active', () => { const component = mount( - + - + , ); jest.runAllTimers(); - expect(component.getDOMNode().querySelector('input')) - .toMatchObject(document.activeElement as HTMLElement); + expect(component.getDOMNode().querySelector('input')).toMatchObject( + document.activeElement as HTMLElement, + ); }); it('calls onExpanded when step is active', () => { - mount(); + mount(); jest.runAllTimers(); - expect(defaultProps.onExpanded) - .toHaveBeenCalledWith('customer'); + expect(defaultProps.onExpanded).toHaveBeenCalledWith('customer'); }); it('scrolls to container when step is active', () => { - const component = mount( - - ); + const component = mount(); jest.runAllTimers(); - const expectedPosition = component.getDOMNode().getBoundingClientRect().top + window.scrollY - window.innerHeight / 5; + const expectedPosition = + component.getDOMNode().getBoundingClientRect().top + + window.scrollY - + window.innerHeight / 5; - expect(window.scrollTo) - .toHaveBeenCalledWith(0, expectedPosition); + expect(window.scrollTo).toHaveBeenCalledWith(0, expectedPosition); }); it('scrolls to container after timeout', () => { - mount(); + mount(); - expect(window.scrollTo) - .not.toHaveBeenCalled(); + expect(window.scrollTo).not.toHaveBeenCalled(); jest.runAllTimers(); - expect(window.scrollTo) - .toHaveBeenCalled(); + expect(window.scrollTo).toHaveBeenCalled(); }); it('does not focus or scroll to element if step is not active', () => { const component = mount( - + - + , ); jest.runAllTimers(); - expect(component.getDOMNode().querySelector('input')) - .not.toEqual(document.activeElement); + expect(component.getDOMNode().querySelector('input')).not.toEqual(document.activeElement); - expect(window.scrollTo) - .not.toHaveBeenCalled(); + expect(window.scrollTo).not.toHaveBeenCalled(); }); it('renders step header with required props', () => { @@ -132,56 +131,37 @@ describe('CheckoutStep', () => { isComplete: true, isEditable: true, onEdit: undefined, - }; - const component = mount( - - ); + const component = mount(); - expect(component.find(CheckoutStepHeader)) - .toHaveLength(1); + expect(component.find(CheckoutStepHeader)).toHaveLength(1); - expect(component.find(CheckoutStepHeader).props()) - .toEqual({ ...headerProps }); + expect(component.find(CheckoutStepHeader).props()).toEqual({ ...headerProps }); }); it('renders content if step is active', () => { - const component = mount( - - Hello world - - ); + const component = mount(Hello world); - expect(component.exists('.checkout-view-content')) - .toEqual(true); + expect(component.exists('.checkout-view-content')).toBe(true); - expect(component.find('.checkout-view-content').text()) - .toEqual('Hello world'); + expect(component.find('.checkout-view-content').text()).toBe('Hello world'); }); it('does not render content if step is not active', () => { const component = mount( - + Hello world - + , ); - expect(component.exists('.checkout-view-content')) - .toEqual(false); + expect(component.exists('.checkout-view-content')).toBe(false); }); it('animates using CSS transition in desktop view', () => { - const component = mount(); + const component = mount(); - expect(component.find(CSSTransition)) - .toHaveLength(1); + expect(component.find(CSSTransition)).toHaveLength(1); expect(component.find(CSSTransition).prop('enter')).toBe(true); expect(component.find(CSSTransition).prop('exit')).toBe(true); }); @@ -189,7 +169,7 @@ describe('CheckoutStep', () => { it('does not animate using CSS transition in mobile view', () => { isMobile = true; - const component = mount(); + const component = mount(); expect(component.find(CSSTransition).prop('enter')).toBe(false); expect(component.find(CSSTransition).prop('exit')).toBe(false); @@ -199,52 +179,36 @@ describe('CheckoutStep', () => { isMobile = true; isMobileView.mockImplementation(() => isMobile); - const component = mount(); + const component = mount(); expect(component.state('isClosed')).toBe(false); - component - .setProps({ isActive: false }) - .update(); + component.setProps({ isActive: false }).update(); expect(component.state('isClosed')).toBe(true); }); it('renders suggestion if step is inactive', () => { const component = shallow( - + , ); - expect(component.find('[data-test="step-suggestion"]').text()) - .toEqual('Billing suggestion'); + expect(component.find('[data-test="step-suggestion"]').text()).toBe('Billing suggestion'); }); it('does not render suggestion if step is active', () => { const component = shallow( - + , ); - expect(component.exists('[data-test="step-suggestion"]')) - .toEqual(false); + expect(component.exists('[data-test="step-suggestion"]')).toBe(false); }); it('does not render suggestion if its not provided', () => { const component = shallow( - + , ); - expect(component.exists('[data-test="step-suggestion"]')) - .toEqual(false); + expect(component.exists('[data-test="step-suggestion"]')).toBe(false); }); }); diff --git a/packages/core/src/app/checkout/CheckoutStep.tsx b/packages/core/src/app/checkout/CheckoutStep.tsx index 4b5cba98cc..198a43036c 100644 --- a/packages/core/src/app/checkout/CheckoutStep.tsx +++ b/packages/core/src/app/checkout/CheckoutStep.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import { noop } from 'lodash'; -import React, { createRef, Component, ReactNode } from 'react'; +import React, { Component, createRef, ReactNode } from 'react'; import { CSSTransition } from 'react-transition-group'; import { isMobileView, MobileView } from '../ui/responsive'; @@ -65,45 +65,37 @@ export default class CheckoutStep extends Component
    - { suggestion && isClosed && !isActive &&
    - { suggestion } -
    } + {suggestion && isClosed && !isActive && ( +
    + {suggestion} +
    + )} - { this.renderContent() } + {this.renderContent()} ); } @@ -111,29 +103,30 @@ export default class CheckoutStep extends Component + return ( - { matched => + {(matched) => (
    - { children } + {children}
    -
    } + + )}
    - ; + ); } private focusStep(): void { @@ -169,7 +162,7 @@ export default class CheckoutStep extends Component('input, select, textarea'); - return input ? input : undefined; + return input || undefined; } private getScrollPosition(): number | undefined { @@ -181,7 +174,8 @@ export default class CheckoutStep extends Component { }); it('renders summary if it is inactive and complete', () => { - const component = shallow( - - ); + const component = shallow(); - expect(component.find('[data-test="step-info"]').text()) - .toEqual('Billing summary'); + expect(component.find('[data-test="step-info"]').text()).toBe('Billing summary'); }); it('does not render summary if it is active', () => { - const component = shallow( - - ); + const component = shallow(); - expect(component.find('[data-test="step-info"]').text()) - .toEqual(''); + expect(component.find('[data-test="step-info"]').text()).toBe(''); }); it('does not render summary if it is inactive or incomplete', () => { - const component = shallow( - - ); + const component = shallow(); - expect(component.find('[data-test="step-info"]').text()) - .toEqual(''); + expect(component.find('[data-test="step-info"]').text()).toBe(''); }); it('renders edit button if it is editable', () => { - const component = shallow( - - ); + const component = shallow(); - expect(component.find(Button).prop('testId')) - .toEqual('step-edit-button'); + expect(component.find(Button).prop('testId')).toBe('step-edit-button'); - expect(component.prop('className')) - .not.toContain('is-readonly'); + expect(component.prop('className')).not.toContain('is-readonly'); - expect(component.prop('className')) - .toContain('is-clickable'); + expect(component.prop('className')).toContain('is-clickable'); }); it('does not render edit button if it is not editable', () => { - const component = shallow( - - ); + const component = shallow(); - expect(component.exists('[data-test="step-edit-button"]')) - .toEqual(false); + expect(component.exists('[data-test="step-edit-button"]')).toBe(false); - expect(component.prop('className')) - .toContain('is-readonly'); + expect(component.prop('className')).toContain('is-readonly'); }); it('triggers callback when clicked', () => { const handleEdit = jest.fn(); const event = { preventDefault: jest.fn() }; const component = shallow( - + , ); component.simulate('click', event); - expect(handleEdit) - .toHaveBeenCalledWith(defaultProps.type); + expect(handleEdit).toHaveBeenCalledWith(defaultProps.type); - expect(event.preventDefault) - .toHaveBeenCalled(); + expect(event.preventDefault).toHaveBeenCalled(); }); it('does not trigger callback when clicked if step is not editable', () => { const handleEdit = jest.fn(); const event = { preventDefault: jest.fn() }; - const component = shallow( - - ); + const component = shallow(); component.simulate('click', event); - expect(handleEdit) - .not.toHaveBeenCalled(); + expect(handleEdit).not.toHaveBeenCalled(); - expect(event.preventDefault) - .toHaveBeenCalled(); + expect(event.preventDefault).toHaveBeenCalled(); }); it('renders "complete" icon if step is complete', () => { - const component = shallow( - - ); + const component = shallow(); - expect(component.find(IconCheck).prop('additionalClassName')) - .toContain('stepHeader-counter--complete'); + expect(component.find(IconCheck).prop('additionalClassName')).toContain( + 'stepHeader-counter--complete', + ); }); it('does not render "complete" icon if step is incomplete', () => { - const component = shallow( - - ); + const component = shallow(); - expect(component.find(IconCheck).prop('additionalClassName')) - .not.toContain('stepHeader-counter--complete'); + expect(component.find(IconCheck).prop('additionalClassName')).not.toContain( + 'stepHeader-counter--complete', + ); }); }); diff --git a/packages/core/src/app/checkout/CheckoutStepHeader.tsx b/packages/core/src/app/checkout/CheckoutStepHeader.tsx index c5448aa799..1f19e72606 100644 --- a/packages/core/src/app/checkout/CheckoutStepHeader.tsx +++ b/packages/core/src/app/checkout/CheckoutStepHeader.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import { noop } from 'lodash'; -import React, { memo, FunctionComponent, ReactNode } from 'react'; +import React, { FunctionComponent, memo, ReactNode } from 'react'; import { preventDefault } from '../common/dom'; import { TranslatedString } from '../locale'; @@ -30,46 +30,43 @@ const CheckoutStepHeader: FunctionComponent = ({ }) => { return (
    onEdit(type) : noop) } + className={classNames('stepHeader', { + 'is-readonly': !isEditable, + 'is-clickable': isEditable && !isActive, + })} + onClick={preventDefault(isEditable && onEdit ? () => onEdit(type) : noop)} >
    -

    - { heading } -

    +

    {heading}

    - { !isActive && isComplete && summary } + {!isActive && isComplete && summary}
    - { isEditable && !isActive &&
    - -
    } + {isEditable && !isActive && ( +
    + +
    + )}
    ); }; diff --git a/packages/core/src/app/checkout/checkouts.mock.ts b/packages/core/src/app/checkout/checkouts.mock.ts index bc8c15d77a..b2772700c4 100644 --- a/packages/core/src/app/checkout/checkouts.mock.ts +++ b/packages/core/src/app/checkout/checkouts.mock.ts @@ -21,9 +21,7 @@ export function getCheckout(): Checkout { ], discounts: [], isStoreCreditApplied: false, - coupons: [ - getCoupon(), - ], + coupons: [getCoupon()], orderId: 295, shippingCostTotal: 15, shippingCostBeforeDiscount: 20, @@ -45,28 +43,26 @@ export function getCheckout(): Checkout { export function getCheckoutWithPayments(): Checkout { return { ...getCheckout(), - payments: [ - getCheckoutPayment(), - ], + payments: [getCheckoutPayment()], }; } export function getCheckoutWithAmazonPay(): Checkout { return { ...getCheckout(), - payments: [{ - ...getCheckoutPayment(), - providerId: 'amazonpay', - }], + payments: [ + { + ...getCheckoutPayment(), + providerId: 'amazonpay', + }, + ], }; } export function getCheckoutWithPromotions(): Checkout { return { ...getCheckout(), - promotions: [ - getPromotion(), - ], + promotions: [getPromotion()], }; } diff --git a/packages/core/src/app/checkout/getCheckoutStepStatuses.spec.ts b/packages/core/src/app/checkout/getCheckoutStepStatuses.spec.ts index e966715b7e..8cd6b0f06a 100644 --- a/packages/core/src/app/checkout/getCheckoutStepStatuses.spec.ts +++ b/packages/core/src/app/checkout/getCheckoutStepStatuses.spec.ts @@ -1,7 +1,14 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, +} from '@bigcommerce/checkout-sdk'; import { find } from 'lodash'; -import { getAddressFormFields, getAddressFormFieldsWithCustomRequired } from '../address/formField.mock'; +import { + getAddressFormFields, + getAddressFormFieldsWithCustomRequired, +} from '../address/formField.mock'; import { getBillingAddress, getEmptyBillingAddress } from '../billing/billingAddresses.mock'; import { getCart } from '../cart/carts.mock'; import { getCustomer, getGuestCustomer } from '../customer/customers.mock'; @@ -11,8 +18,8 @@ import { getConsignment } from '../shipping/consignment.mock'; import { getShippingAddress } from '../shipping/shipping-addresses.mock'; import { getCheckoutWithAmazonPay, getCheckoutWithPayments } from './checkouts.mock'; -import getCheckoutStepStatuses from './getCheckoutStepStatuses'; import CheckoutStepType from './CheckoutStepType'; +import getCheckoutStepStatuses from './getCheckoutStepStatuses'; describe('getCheckoutStepStatuses()', () => { let service: CheckoutService; @@ -25,489 +32,431 @@ describe('getCheckoutStepStatuses()', () => { describe('customer step', () => { it('is marked as complete if email is provided by guest shopper', () => { - jest.spyOn(state.data, 'getBillingAddress') - .mockReturnValue(getBillingAddress()); + jest.spyOn(state.data, 'getBillingAddress').mockReturnValue(getBillingAddress()); const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Customer })!.isComplete) - .toEqual(true); + expect(find(steps, { type: CheckoutStepType.Customer })!.isComplete).toBe(true); }); it('is marked as complete if email is provided by returning shopper', () => { - jest.spyOn(state.data, 'getCustomer') - .mockReturnValue(getCustomer()); + jest.spyOn(state.data, 'getCustomer').mockReturnValue(getCustomer()); const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Customer })!.isComplete) - .toEqual(true); + expect(find(steps, { type: CheckoutStepType.Customer })!.isComplete).toBe(true); }); it('is marked as complete if email is provided by digital wallet', () => { - jest.spyOn(state.data, 'getCheckout') - .mockReturnValue(getCheckoutWithPayments()); + jest.spyOn(state.data, 'getCheckout').mockReturnValue(getCheckoutWithPayments()); const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Customer })!.isComplete) - .toEqual(true); + expect(find(steps, { type: CheckoutStepType.Customer })!.isComplete).toBe(true); }); it('is marked as incomplete if email is not provided', () => { const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Customer })!.isComplete) - .toEqual(false); + expect(find(steps, { type: CheckoutStepType.Customer })!.isComplete).toBe(false); }); it('is marked as editable if email is provided by guest shopper directly', () => { - jest.spyOn(state.data, 'getCustomer') - .mockReturnValue(getGuestCustomer()); + jest.spyOn(state.data, 'getCustomer').mockReturnValue(getGuestCustomer()); // Email address is surfaced through billing address for guest shoppers - jest.spyOn(state.data, 'getBillingAddress') - .mockReturnValue(getBillingAddress()); + jest.spyOn(state.data, 'getBillingAddress').mockReturnValue(getBillingAddress()); const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Customer })!.isEditable) - .toEqual(true); + expect(find(steps, { type: CheckoutStepType.Customer })!.isEditable).toBe(true); }); it('is marked as non-editable if email is provided by digital wallet', () => { - jest.spyOn(state.data, 'getCheckout') - .mockReturnValue(getCheckoutWithPayments()); + jest.spyOn(state.data, 'getCheckout').mockReturnValue(getCheckoutWithPayments()); const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Customer })!.isEditable) - .toEqual(false); + expect(find(steps, { type: CheckoutStepType.Customer })!.isEditable).toBe(false); }); it('is marked as non-editable if step is incomplete', () => { const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Customer })!.isEditable) - .toEqual(false); + expect(find(steps, { type: CheckoutStepType.Customer })!.isEditable).toBe(false); }); }); describe('billing step', () => { it('is marked as complete if billing address is provided', () => { - jest.spyOn(state.data, 'getBillingAddress') - .mockReturnValue(getBillingAddress()); + jest.spyOn(state.data, 'getBillingAddress').mockReturnValue(getBillingAddress()); const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Billing })!.isComplete) - .toEqual(true); + expect(find(steps, { type: CheckoutStepType.Billing })!.isComplete).toBe(true); }); it('is marked as complete if billing address is provided by digital wallet', () => { - jest.spyOn(state.data, 'getCheckout') - .mockReturnValue(getCheckoutWithPayments()); + jest.spyOn(state.data, 'getCheckout').mockReturnValue(getCheckoutWithPayments()); const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Billing })!.isComplete) - .toEqual(true); + expect(find(steps, { type: CheckoutStepType.Billing })!.isComplete).toBe(true); }); it('is marked as incomplete if billing address is not provided', () => { const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Billing })!.isComplete) - .toEqual(false); + expect(find(steps, { type: CheckoutStepType.Billing })!.isComplete).toBe(false); }); it('is marked as non-editable if step is incomplete', () => { const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Billing })!.isEditable) - .toEqual(false); + expect(find(steps, { type: CheckoutStepType.Billing })!.isEditable).toBe(false); }); describe('amazonpay', () => { it('is marked as complete if billing address is not provided', () => { - jest.spyOn(state.data, 'getCheckout') - .mockReturnValue(getCheckoutWithAmazonPay()); + jest.spyOn(state.data, 'getCheckout').mockReturnValue(getCheckoutWithAmazonPay()); const steps = getCheckoutStepStatuses(state); - expect(find(steps, { type: CheckoutStepType.Billing })) - .toHaveProperty('isComplete', true); + expect(find(steps, { type: CheckoutStepType.Billing })).toHaveProperty( + 'isComplete', + true, + ); }); it('is marked as complete if billing address is not provided and custom fields are valid', () => { - jest.spyOn(state.data, 'getCheckout') - .mockReturnValue(getCheckoutWithAmazonPay()); - jest.spyOn(state.data, 'getBillingAddress') - .mockReturnValue({ - ...getEmptyBillingAddress(), - customFields: [{ fieldId: 'foo', fieldValue: 'foo' }], - }); - jest.spyOn(state.data, 'getBillingAddressFields') - .mockReturnValue(getAddressFormFieldsWithCustomRequired()); + jest.spyOn(state.data, 'getCheckout').mockReturnValue(getCheckoutWithAmazonPay()); + jest.spyOn(state.data, 'getBillingAddress').mockReturnValue({ + ...getEmptyBillingAddress(), + customFields: [{ fieldId: 'foo', fieldValue: 'foo' }], + }); + jest.spyOn(state.data, 'getBillingAddressFields').mockReturnValue( + getAddressFormFieldsWithCustomRequired(), + ); const steps = getCheckoutStepStatuses(state); - expect(find(steps, { type: CheckoutStepType.Billing })) - .toHaveProperty('isComplete', true); + expect(find(steps, { type: CheckoutStepType.Billing })).toHaveProperty( + 'isComplete', + true, + ); }); it('is marked as incomplete if billing address is not provided but custom fields are invalid', () => { - jest.spyOn(state.data, 'getCheckout') - .mockReturnValue(getCheckoutWithAmazonPay()); - jest.spyOn(state.data, 'getBillingAddress') - .mockReturnValue({ - ...getEmptyBillingAddress(), - customFields: [{ fieldId: 'foo', fieldValue: '' }], - }); - jest.spyOn(state.data, 'getBillingAddressFields') - .mockReturnValue(getAddressFormFieldsWithCustomRequired()); + jest.spyOn(state.data, 'getCheckout').mockReturnValue(getCheckoutWithAmazonPay()); + jest.spyOn(state.data, 'getBillingAddress').mockReturnValue({ + ...getEmptyBillingAddress(), + customFields: [{ fieldId: 'foo', fieldValue: '' }], + }); + jest.spyOn(state.data, 'getBillingAddressFields').mockReturnValue( + getAddressFormFieldsWithCustomRequired(), + ); const steps = getCheckoutStepStatuses(state); - expect(find(steps, { type: CheckoutStepType.Billing })) - .toHaveProperty('isComplete', false); + expect(find(steps, { type: CheckoutStepType.Billing })).toHaveProperty( + 'isComplete', + false, + ); }); it('is marked as non-editable if step is complete and there are no custom fields', () => { - jest.spyOn(state.data, 'getCheckout') - .mockReturnValue(getCheckoutWithAmazonPay()); - jest.spyOn(state.data, 'getBillingAddress') - .mockReturnValue(getBillingAddress()); + jest.spyOn(state.data, 'getCheckout').mockReturnValue(getCheckoutWithAmazonPay()); + jest.spyOn(state.data, 'getBillingAddress').mockReturnValue(getBillingAddress()); const steps = getCheckoutStepStatuses(state); - expect(find(steps, { type: CheckoutStepType.Billing })) - .toHaveProperty('isEditable', false); + expect(find(steps, { type: CheckoutStepType.Billing })).toHaveProperty( + 'isEditable', + false, + ); }); it('is marked as editable if step is complete and there is custom fields', () => { - jest.spyOn(state.data, 'getCheckout') - .mockReturnValue(getCheckoutWithAmazonPay()); - jest.spyOn(state.data, 'getBillingAddress') - .mockReturnValue(getBillingAddress()); - jest.spyOn(state.data, 'getBillingAddressFields') - .mockReturnValue(getAddressFormFields()); + jest.spyOn(state.data, 'getCheckout').mockReturnValue(getCheckoutWithAmazonPay()); + jest.spyOn(state.data, 'getBillingAddress').mockReturnValue(getBillingAddress()); + jest.spyOn(state.data, 'getBillingAddressFields').mockReturnValue( + getAddressFormFields(), + ); const steps = getCheckoutStepStatuses(state); - expect(find(steps, { type: CheckoutStepType.Billing })) - .toHaveProperty('isEditable', true); + expect(find(steps, { type: CheckoutStepType.Billing })).toHaveProperty( + 'isEditable', + true, + ); }); }); }); describe('shipping step', () => { it('is marked as complete if shipping address and option are provided and there are no unassigned line items', () => { - jest.spyOn(state.data, 'getShippingAddress') - .mockReturnValue(getShippingAddress()); + jest.spyOn(state.data, 'getShippingAddress').mockReturnValue(getShippingAddress()); - jest.spyOn(state.data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(state.data, 'getCart').mockReturnValue(getCart()); - jest.spyOn(state.data, 'getConsignments') - .mockReturnValue([getConsignment()]); + jest.spyOn(state.data, 'getConsignments').mockReturnValue([getConsignment()]); const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Shipping })!.isComplete) - .toEqual(true); + expect(find(steps, { type: CheckoutStepType.Shipping })!.isComplete).toBe(true); }); it('is marked as incomplete if shipping address is not provided', () => { - jest.spyOn(state.data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(state.data, 'getCart').mockReturnValue(getCart()); - jest.spyOn(state.data, 'getConsignments') - .mockReturnValue([getConsignment()]); + jest.spyOn(state.data, 'getConsignments').mockReturnValue([getConsignment()]); const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Shipping })!.isComplete) - .toEqual(false); + expect(find(steps, { type: CheckoutStepType.Shipping })!.isComplete).toBe(false); }); it('is marked as complete if shipping address is not valid but payment is amazon', () => { - jest.spyOn(state.data, 'getShippingAddress') - .mockReturnValue({ - ...getShippingAddress(), - address1: '', - }); + jest.spyOn(state.data, 'getShippingAddress').mockReturnValue({ + ...getShippingAddress(), + address1: '', + }); - jest.spyOn(state.data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(state.data, 'getCart').mockReturnValue(getCart()); - jest.spyOn(state.data, 'getSelectedPaymentMethod') - .mockReturnValue({ - ...getPaymentMethod(), - id: 'amazon', - }); + jest.spyOn(state.data, 'getSelectedPaymentMethod').mockReturnValue({ + ...getPaymentMethod(), + id: 'amazon', + }); - jest.spyOn(state.data, 'getConsignments') - .mockReturnValue([getConsignment()]); + jest.spyOn(state.data, 'getConsignments').mockReturnValue([getConsignment()]); const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Shipping })!.isComplete) - .toEqual(true); + expect(find(steps, { type: CheckoutStepType.Shipping })!.isComplete).toBe(true); }); it('is marked as incomplete if shipping option is not provided', () => { - jest.spyOn(state.data, 'getShippingAddress') - .mockReturnValue(getShippingAddress()); + jest.spyOn(state.data, 'getShippingAddress').mockReturnValue(getShippingAddress()); - jest.spyOn(state.data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(state.data, 'getCart').mockReturnValue(getCart()); - jest.spyOn(state.data, 'getConsignments') - .mockReturnValue([{ + jest.spyOn(state.data, 'getConsignments').mockReturnValue([ + { ...getConsignment(), selectedShippingOption: undefined, - }]); + }, + ]); const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Shipping })!.isComplete) - .toEqual(false); + expect(find(steps, { type: CheckoutStepType.Shipping })!.isComplete).toBe(false); }); it('is marked as incomplete if there are unassigned line items', () => { - jest.spyOn(state.data, 'getShippingAddress') - .mockReturnValue(getShippingAddress()); + jest.spyOn(state.data, 'getShippingAddress').mockReturnValue(getShippingAddress()); - jest.spyOn(state.data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(state.data, 'getCart').mockReturnValue(getCart()); - jest.spyOn(state.data, 'getConsignments') - .mockReturnValue([{ + jest.spyOn(state.data, 'getConsignments').mockReturnValue([ + { ...getConsignment(), lineItemIds: [], - }]); + }, + ]); const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Shipping })!.isComplete) - .toEqual(false); + expect(find(steps, { type: CheckoutStepType.Shipping })!.isComplete).toBe(false); }); it('is marked as required if cart contains physical items', () => { - jest.spyOn(state.data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(state.data, 'getCart').mockReturnValue(getCart()); const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Shipping })!.isRequired) - .toEqual(true); + expect(find(steps, { type: CheckoutStepType.Shipping })!.isRequired).toBe(true); }); it('is marked as not required if cart does not contain physical items', () => { - jest.spyOn(state.data, 'getCart') - .mockReturnValue({ - ...getCart(), - lineItems: { - ...getCart().lineItems, - physicalItems: [], - }, - }); + jest.spyOn(state.data, 'getCart').mockReturnValue({ + ...getCart(), + lineItems: { + ...getCart().lineItems, + physicalItems: [], + }, + }); const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Shipping })!.isRequired) - .toEqual(false); + expect(find(steps, { type: CheckoutStepType.Shipping })!.isRequired).toBe(false); }); it('is marked as not editable if cart does not contain physical items', () => { - jest.spyOn(state.data, 'getCustomer') - .mockReturnValue(getCustomer()); + jest.spyOn(state.data, 'getCustomer').mockReturnValue(getCustomer()); - jest.spyOn(state.data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(state.data, 'getCart').mockReturnValue(getCart()); - jest.spyOn(state.data, 'getConsignments') - .mockReturnValue([getConsignment()]); + jest.spyOn(state.data, 'getConsignments').mockReturnValue([getConsignment()]); - jest.spyOn(state.data, 'getShippingAddress') - .mockReturnValue(getShippingAddress()); + jest.spyOn(state.data, 'getShippingAddress').mockReturnValue(getShippingAddress()); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(getCheckoutStepStatuses(state), { type: CheckoutStepType.Shipping })!.isEditable) - .toEqual(true); + expect( + find(getCheckoutStepStatuses(state), { type: CheckoutStepType.Shipping })! + .isEditable, + ).toBe(true); // Mock new state state = { ...state }; - jest.spyOn(state.data, 'getCart') - .mockReturnValue({ - ...getCart(), - lineItems: { - ...getCart().lineItems, - physicalItems: [], - }, - }); + jest.spyOn(state.data, 'getCart').mockReturnValue({ + ...getCart(), + lineItems: { + ...getCart().lineItems, + physicalItems: [], + }, + }); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(getCheckoutStepStatuses(state), { type: CheckoutStepType.Shipping })!.isEditable) - .toEqual(false); + expect( + find(getCheckoutStepStatuses(state), { type: CheckoutStepType.Shipping })! + .isEditable, + ).toBe(false); }); it('is marked as non-editable if step is incomplete', () => { const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Shipping })!.isEditable) - .toEqual(false); + expect(find(steps, { type: CheckoutStepType.Shipping })!.isEditable).toBe(false); }); }); describe('payment step', () => { it('is marked as complete if order is complete', () => { - jest.spyOn(state.data, 'getOrder') - .mockReturnValue(getOrder()); + jest.spyOn(state.data, 'getOrder').mockReturnValue(getOrder()); const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Payment })!.isComplete) - .toEqual(true); + expect(find(steps, { type: CheckoutStepType.Payment })!.isComplete).toBe(true); }); it('is marked as incomplete if order is incomplete', () => { - jest.spyOn(state.data, 'getOrder') - .mockReturnValue({ - ...getOrder(), - isComplete: false, - }); + jest.spyOn(state.data, 'getOrder').mockReturnValue({ + ...getOrder(), + isComplete: false, + }); const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Payment })!.isComplete) - .toEqual(false); + expect(find(steps, { type: CheckoutStepType.Payment })!.isComplete).toBe(false); }); it('is marked as non-editable if step is incomplete', () => { const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Payment })!.isEditable) - .toEqual(false); + expect(find(steps, { type: CheckoutStepType.Payment })!.isEditable).toBe(false); }); }); it('returns steps in order', () => { const steps = getCheckoutStepStatuses(state); - expect(steps.map(step => step.type)) - .toEqual([ - CheckoutStepType.Customer, - CheckoutStepType.Shipping, - CheckoutStepType.Billing, - CheckoutStepType.Payment, - ]); + expect(steps.map((step) => step.type)).toEqual([ + CheckoutStepType.Customer, + CheckoutStepType.Shipping, + CheckoutStepType.Billing, + CheckoutStepType.Payment, + ]); }); it('marks latter steps as non-editable if earlier steps are incomplete', () => { - jest.spyOn(state.data, 'getShippingAddress') - .mockReturnValue(undefined); + jest.spyOn(state.data, 'getShippingAddress').mockReturnValue(undefined); - jest.spyOn(state.data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(state.data, 'getCart').mockReturnValue(getCart()); - jest.spyOn(state.data, 'getConsignments') - .mockReturnValue([getConsignment()]); + jest.spyOn(state.data, 'getConsignments').mockReturnValue([getConsignment()]); const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Billing })!.isEditable) - .toEqual(false); + expect(find(steps, { type: CheckoutStepType.Billing })!.isEditable).toBe(false); }); it('does not mark latter steps as non-editable if earlier steps are complete', () => { - jest.spyOn(state.data, 'getBillingAddress') - .mockReturnValue(getBillingAddress()); + jest.spyOn(state.data, 'getBillingAddress').mockReturnValue(getBillingAddress()); - jest.spyOn(state.data, 'getShippingAddress') - .mockReturnValue(getShippingAddress()); + jest.spyOn(state.data, 'getShippingAddress').mockReturnValue(getShippingAddress()); - jest.spyOn(state.data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(state.data, 'getCart').mockReturnValue(getCart()); - jest.spyOn(state.data, 'getConsignments') - .mockReturnValue([getConsignment()]); + jest.spyOn(state.data, 'getConsignments').mockReturnValue([getConsignment()]); const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Billing })!.isEditable) - .toEqual(true); + expect(find(steps, { type: CheckoutStepType.Billing })!.isEditable).toBe(true); }); it('marks first incomplete step as active', () => { const steps = getCheckoutStepStatuses(state); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(steps, { type: CheckoutStepType.Customer })!.isActive) - .toEqual(true); + expect(find(steps, { type: CheckoutStepType.Customer })!.isActive).toBe(true); }); it('does not mark incomplete step as active if it is not required', () => { - jest.spyOn(state.data, 'getCustomer') - .mockReturnValue(getCustomer()); + jest.spyOn(state.data, 'getCustomer').mockReturnValue(getCustomer()); // If cart has physical items, shipping step should be active - jest.spyOn(state.data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(state.data, 'getCart').mockReturnValue(getCart()); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(getCheckoutStepStatuses(state), { type: CheckoutStepType.Shipping })!.isActive) - .toEqual(true); + expect( + find(getCheckoutStepStatuses(state), { type: CheckoutStepType.Shipping })!.isActive, + ).toBe(true); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(getCheckoutStepStatuses(state), { type: CheckoutStepType.Billing })!.isActive) - .toEqual(false); + expect( + find(getCheckoutStepStatuses(state), { type: CheckoutStepType.Billing })!.isActive, + ).toBe(false); // Mock a new state state = { ...state }; // If cart has no physical item, shipping step shouldn't be active - jest.spyOn(state.data, 'getCart') - .mockReturnValue({ - ...getCart(), - lineItems: { - ...getCart().lineItems, - physicalItems: [], - }, - }); + jest.spyOn(state.data, 'getCart').mockReturnValue({ + ...getCart(), + lineItems: { + ...getCart().lineItems, + physicalItems: [], + }, + }); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(getCheckoutStepStatuses(state), { type: CheckoutStepType.Shipping })!.isActive) - .toEqual(false); + expect( + find(getCheckoutStepStatuses(state), { type: CheckoutStepType.Shipping })!.isActive, + ).toBe(false); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(find(getCheckoutStepStatuses(state), { type: CheckoutStepType.Billing })!.isActive) - .toEqual(true); + expect( + find(getCheckoutStepStatuses(state), { type: CheckoutStepType.Billing })!.isActive, + ).toBe(true); }); }); diff --git a/packages/core/src/app/checkout/getCheckoutStepStatuses.ts b/packages/core/src/app/checkout/getCheckoutStepStatuses.ts index db2f21b1bb..126a8d5570 100644 --- a/packages/core/src/app/checkout/getCheckoutStepStatuses.ts +++ b/packages/core/src/app/checkout/getCheckoutStepStatuses.ts @@ -5,7 +5,11 @@ import { createSelector } from 'reselect'; import { isValidAddress } from '../address'; import { EMPTY_ARRAY } from '../common/utility'; import { SUPPORTED_METHODS } from '../customer'; -import { hasSelectedShippingOptions, hasUnassignedLineItems, itemsRequireShipping } from '../shipping'; +import { + hasSelectedShippingOptions, + hasUnassignedLineItems, + itemsRequireShipping, +} from '../shipping'; import CheckoutStepType from './CheckoutStepType'; @@ -14,8 +18,16 @@ const getCustomerStepStatus = createSelector( ({ data }: CheckoutSelectors) => data.getCustomer(), ({ data }: CheckoutSelectors) => data.getBillingAddress(), (checkout, customer, billingAddress) => { - const hasEmail = !!(customer && customer.email || billingAddress && billingAddress.email); - const isUsingWallet = checkout && checkout.payments ? checkout.payments.some(payment => SUPPORTED_METHODS.indexOf(payment.providerId) >= 0) : false; + const hasEmail = !!( + (customer && customer.email) || + (billingAddress && billingAddress.email) + ); + const isUsingWallet = + checkout && checkout.payments + ? checkout.payments.some( + (payment) => SUPPORTED_METHODS.indexOf(payment.providerId) >= 0, + ) + : false; const isGuest = !!(customer && customer.isGuest); const isComplete = hasEmail || isUsingWallet; @@ -26,7 +38,7 @@ const getCustomerStepStatus = createSelector( isEditable: isComplete && !isUsingWallet && isGuest, isRequired: true, }; - } + }, ); const getBillingStepStatus = createSelector( @@ -35,18 +47,35 @@ const getBillingStepStatus = createSelector( ({ data }: CheckoutSelectors) => { const billingAddress = data.getBillingAddress(); - return billingAddress ? data.getBillingAddressFields(billingAddress.countryCode) : EMPTY_ARRAY; + return billingAddress + ? data.getBillingAddressFields(billingAddress.countryCode) + : EMPTY_ARRAY; }, (checkout, billingAddress, billingAddressFields) => { - const hasAddress = billingAddress ? isValidAddress(billingAddress, billingAddressFields) : false; - const isUsingWallet = checkout && checkout.payments ? checkout.payments.some(payment => SUPPORTED_METHODS.indexOf(payment.providerId) >= 0) : false; + const hasAddress = billingAddress + ? isValidAddress(billingAddress, billingAddressFields) + : false; + const isUsingWallet = + checkout && checkout.payments + ? checkout.payments.some( + (payment) => SUPPORTED_METHODS.indexOf(payment.providerId) >= 0, + ) + : false; const isComplete = hasAddress || isUsingWallet; - const isUsingAmazonPay = checkout && checkout.payments ? checkout.payments.some(payment => payment.providerId === 'amazonpay') : false; + const isUsingAmazonPay = + checkout && checkout.payments + ? checkout.payments.some((payment) => payment.providerId === 'amazonpay') + : false; if (isUsingAmazonPay) { - const billingAddressCustomFields = billingAddressFields.filter(({ custom }: { custom: boolean }) => custom); + const billingAddressCustomFields = billingAddressFields.filter( + ({ custom }: { custom: boolean }) => custom, + ); const hasCustomFields = billingAddressCustomFields.length > 0; - const isAmazonPayBillingStepComplete = billingAddress && hasCustomFields ? isValidAddress(billingAddress, billingAddressCustomFields) : true; + const isAmazonPayBillingStepComplete = + billingAddress && hasCustomFields + ? isValidAddress(billingAddress, billingAddressCustomFields) + : true; return { type: CheckoutStepType.Billing, @@ -64,7 +93,7 @@ const getBillingStepStatus = createSelector( isEditable: isComplete && !isUsingWallet, isRequired: true, }; - } + }, ); const getShippingStepStatus = createSelector( @@ -75,15 +104,20 @@ const getShippingStepStatus = createSelector( ({ data }: CheckoutSelectors) => { const shippingAddress = data.getShippingAddress(); - return shippingAddress ? data.getShippingAddressFields(shippingAddress.countryCode) : EMPTY_ARRAY; + return shippingAddress + ? data.getShippingAddressFields(shippingAddress.countryCode) + : EMPTY_ARRAY; }, ({ data }: CheckoutSelectors) => data.getConfig(), (shippingAddress, consignments, cart, payment, shippingAddressFields, config) => { - const hasAddress = shippingAddress ? isValidAddress(shippingAddress, shippingAddressFields) : false; + const hasAddress = shippingAddress + ? isValidAddress(shippingAddress, shippingAddressFields) + : false; // @todo: interim solution, ideally we should render custom form fields below amazon shipping widget const hasRemoteAddress = !!shippingAddress && !!payment && payment.id === 'amazon'; const hasOptions = consignments ? hasSelectedShippingOptions(consignments) : false; - const hasUnassignedItems = cart && consignments ? hasUnassignedLineItems(consignments, cart.lineItems) : true; + const hasUnassignedItems = + cart && consignments ? hasUnassignedLineItems(consignments, cart.lineItems) : true; const isComplete = (hasAddress || hasRemoteAddress) && hasOptions && !hasUnassignedItems; const isRequired = itemsRequireShipping(cart, config); @@ -94,12 +128,12 @@ const getShippingStepStatus = createSelector( isEditable: isComplete && isRequired, isRequired, }; - } + }, ); const getPaymentStepStatus = createSelector( ({ data }: CheckoutSelectors) => data.getOrder(), - order => { + (order) => { const isComplete = order ? order.isComplete : false; return { @@ -109,7 +143,7 @@ const getPaymentStepStatus = createSelector( isEditable: isComplete, isRequired: true, }; - } + }, ); const getCheckoutStepStatuses = createSelector( @@ -118,17 +152,15 @@ const getCheckoutStepStatuses = createSelector( getBillingStepStatus, getPaymentStepStatus, (customerStep, shippingStep, billingStep, paymentStep) => { - const steps = compact([ - customerStep, - shippingStep, - billingStep, - paymentStep, - ]); + const steps = compact([customerStep, shippingStep, billingStep, paymentStep]); - const defaultActiveStep = steps.find(step => !step.isComplete && step.isRequired) || steps[steps.length - 1]; + const defaultActiveStep = + steps.find((step) => !step.isComplete && step.isRequired) || steps[steps.length - 1]; return steps.map((step, index) => { - const isPrevStepComplete = steps.slice(0, index).every(prevStep => prevStep.isComplete || !prevStep.isRequired); + const isPrevStepComplete = steps + .slice(0, index) + .every((prevStep) => prevStep.isComplete || !prevStep.isRequired); return { ...step, @@ -138,7 +170,7 @@ const getCheckoutStepStatuses = createSelector( isEditable: isPrevStepComplete && step.isEditable, }; }); - } + }, ); export default getCheckoutStepStatuses; diff --git a/packages/core/src/app/checkout/index.ts b/packages/core/src/app/checkout/index.ts index 1999ff0f22..75e583a1f3 100644 --- a/packages/core/src/app/checkout/index.ts +++ b/packages/core/src/app/checkout/index.ts @@ -1,6 +1,10 @@ export { RenderCheckout, RenderCheckoutOptions } from './renderCheckout'; export { default as CheckoutContext, CheckoutContextProps } from './CheckoutContext'; -export { default as CheckoutProvider, CheckoutProviderProps, CheckoutProviderState } from './CheckoutProvider'; +export { + default as CheckoutProvider, + CheckoutProviderProps, + CheckoutProviderState, +} from './CheckoutProvider'; export { default as withCheckout, WithCheckoutProps } from './withCheckout'; export { default as CheckoutSupport } from './CheckoutSupport'; export { default as NoopCheckoutSupport } from './NoopCheckoutSupport'; diff --git a/packages/core/src/app/checkout/mapToCheckoutProps.spec.ts b/packages/core/src/app/checkout/mapToCheckoutProps.spec.ts index d7a9c035ef..69d9771b9b 100644 --- a/packages/core/src/app/checkout/mapToCheckoutProps.spec.ts +++ b/packages/core/src/app/checkout/mapToCheckoutProps.spec.ts @@ -1,7 +1,12 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService, CustomError } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, + CustomError, +} from '@bigcommerce/checkout-sdk'; -import mapToCheckoutProps from './mapToCheckoutProps'; import { CheckoutContextProps } from './CheckoutContext'; +import mapToCheckoutProps from './mapToCheckoutProps'; describe('mapToCheckoutProps()', () => { let checkoutService: CheckoutService; @@ -19,22 +24,22 @@ describe('mapToCheckoutProps()', () => { }); it('returns true if unable to submit order because cart has changed', () => { - jest.spyOn(checkoutState.errors, 'getSubmitOrderError') - .mockReturnValue({ type: 'cart_changed' } as CustomError); + jest.spyOn(checkoutState.errors, 'getSubmitOrderError').mockReturnValue({ + type: 'cart_changed', + } as CustomError); const { hasCartChanged } = mapToCheckoutProps(contextProps); - expect(hasCartChanged) - .toEqual(true); + expect(hasCartChanged).toBe(true); }); it('returns false if unable to submit order because of other causes', () => { - jest.spyOn(checkoutState.errors, 'getSubmitOrderError') - .mockReturnValue({ type: 'unknown' } as CustomError); + jest.spyOn(checkoutState.errors, 'getSubmitOrderError').mockReturnValue({ + type: 'unknown', + } as CustomError); const { hasCartChanged } = mapToCheckoutProps(contextProps); - expect(hasCartChanged) - .toEqual(false); + expect(hasCartChanged).toBe(false); }); }); diff --git a/packages/core/src/app/checkout/mapToCheckoutProps.ts b/packages/core/src/app/checkout/mapToCheckoutProps.ts index ef8293a9eb..9f96b63799 100644 --- a/packages/core/src/app/checkout/mapToCheckoutProps.ts +++ b/packages/core/src/app/checkout/mapToCheckoutProps.ts @@ -3,36 +3,32 @@ import { createSelector } from 'reselect'; import { EMPTY_ARRAY } from '../common/utility'; -import getCheckoutStepStatuses from './getCheckoutStepStatuses'; import { WithCheckoutProps } from './Checkout'; import { CheckoutContextProps } from './CheckoutContext'; +import getCheckoutStepStatuses from './getCheckoutStepStatuses'; -export default function mapToCheckoutProps( - { checkoutService, checkoutState }: CheckoutContextProps -): WithCheckoutProps { +export default function mapToCheckoutProps({ + checkoutService, + checkoutState, +}: CheckoutContextProps): WithCheckoutProps { const { data, errors, statuses } = checkoutState; const { promotions = EMPTY_ARRAY } = data.getCheckout() || {}; const submitOrderError = errors.getSubmitOrderError() as CustomError; const { - checkoutSettings: { - guestCheckoutEnabled: isGuestEnabled = false, - features = {}, - } = {}, + checkoutSettings: { guestCheckoutEnabled: isGuestEnabled = false, features = {} } = {}, links: { loginLink: loginUrl = '', createAccountLink: createAccountUrl = '', cartLink: cartUrl = '', } = {}, - displaySettings: { - hidePriceFromGuests: isPriceHiddenFromGuests = false, - } = {} + displaySettings: { hidePriceFromGuests: isPriceHiddenFromGuests = false } = {}, } = data.getConfig() || {}; const subscribeToConsignmentsSelector = createSelector( - ({ checkoutService: { subscribe} }: CheckoutContextProps) => subscribe, - subscribe => (subscriber: (state: CheckoutSelectors) => void) => { + ({ checkoutService: { subscribe } }: CheckoutContextProps) => subscribe, + (subscribe) => (subscriber: (state: CheckoutSelectors) => void) => { return subscribe(subscriber, ({ data: { getConsignments } }) => getConsignments()); - } + }, ); return { @@ -51,7 +47,10 @@ export default function mapToCheckoutProps( createAccountUrl, canCreateAccountInCheckout: features['CHECKOUT-4941.account_creation_in_checkout'], promotions, - subscribeToConsignments: subscribeToConsignmentsSelector({ checkoutService, checkoutState }), + subscribeToConsignments: subscribeToConsignmentsSelector({ + checkoutService, + checkoutState, + }), steps: data.getCheckout() ? getCheckoutStepStatuses(checkoutState) : EMPTY_ARRAY, }; } diff --git a/packages/core/src/app/checkout/navigateToOrderConfirmation.spec.tsx b/packages/core/src/app/checkout/navigateToOrderConfirmation.spec.tsx index 5ff3e2d616..d4da9ef25f 100644 --- a/packages/core/src/app/checkout/navigateToOrderConfirmation.spec.tsx +++ b/packages/core/src/app/checkout/navigateToOrderConfirmation.spec.tsx @@ -15,16 +15,14 @@ describe('navigateToOrderConfirmation', () => { it('navigates to order confirmation page based on its current path', () => { navigateToOrderConfirmation(true); - expect(window.location.replace) - .toHaveBeenCalledWith('/checkout/order-confirmation'); + expect(window.location.replace).toHaveBeenCalledWith('/checkout/order-confirmation'); }); it('navigates to order confirmation page with orderId in the URL when it is a buy now cart checkout', () => { window.location.pathname = '/checkout/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'; navigateToOrderConfirmation(true, 100); - expect(window.location.replace) - .toHaveBeenCalledWith('/checkout/order-confirmation/100'); + expect(window.location.replace).toHaveBeenCalledWith('/checkout/order-confirmation/100'); }); it('discards any query params when navigating to order confirmation page', () => { @@ -33,7 +31,8 @@ describe('navigateToOrderConfirmation', () => { navigateToOrderConfirmation(true); - expect(window.location.replace) - .toHaveBeenCalledWith('/embedded-checkout/order-confirmation'); + expect(window.location.replace).toHaveBeenCalledWith( + '/embedded-checkout/order-confirmation', + ); }); }); diff --git a/packages/core/src/app/checkout/navigateToOrderConfirmation.tsx b/packages/core/src/app/checkout/navigateToOrderConfirmation.tsx index 91e38e9758..a853ec233e 100644 --- a/packages/core/src/app/checkout/navigateToOrderConfirmation.tsx +++ b/packages/core/src/app/checkout/navigateToOrderConfirmation.tsx @@ -2,7 +2,10 @@ import { noop } from 'lodash'; import { isBuyNowCart } from '../common/utility'; -export default function navigateToOrderConfirmation(isBuyNowCartEnabled: boolean = false, orderId?: number): Promise { +export default function navigateToOrderConfirmation( + isBuyNowCartEnabled = false, + orderId?: number, +): Promise { let url: string; if (isBuyNowCartEnabled) { @@ -11,6 +14,7 @@ export default function navigateToOrderConfirmation(isBuyNowCartEnabled: boolean } else { url = `${window.location.pathname.replace(/\/$/, '')}/order-confirmation`; } + window.location.replace(url); return new Promise(noop); diff --git a/packages/core/src/app/checkout/renderCheckout.spec.tsx b/packages/core/src/app/checkout/renderCheckout.spec.tsx index f2c236fbb9..0e451e24c7 100644 --- a/packages/core/src/app/checkout/renderCheckout.spec.tsx +++ b/packages/core/src/app/checkout/renderCheckout.spec.tsx @@ -1,7 +1,7 @@ import React, { FunctionComponent } from 'react'; -import renderCheckout, { RenderCheckoutOptions } from './renderCheckout'; import { CheckoutAppProps } from './CheckoutApp'; +import renderCheckout, { RenderCheckoutOptions } from './renderCheckout'; let CheckoutApp: FunctionComponent; let configurePublicPath: (path: string) => void; @@ -10,7 +10,7 @@ let publicPath: string; jest.mock('@welldone-software/why-did-you-render', () => jest.fn()); jest.mock('../common/bundler', () => { - configurePublicPath = jest.fn(path => { + configurePublicPath = jest.fn((path) => { publicPath = path; return publicPath; @@ -22,7 +22,7 @@ jest.mock('../common/bundler', () => { }); jest.mock('./CheckoutApp', () => { - CheckoutApp = jest.fn(() => <>{ publicPath }); + CheckoutApp = jest.fn(() => <>{publicPath}); return { default: CheckoutApp, @@ -55,25 +55,21 @@ describe('renderCheckout()', () => { it('configures public path before mounting app component', () => { renderCheckout(options); - expect(configurePublicPath) - .toHaveBeenCalledWith(options.publicPath); + expect(configurePublicPath).toHaveBeenCalledWith(options.publicPath); - expect(container.innerHTML) - .toEqual(options.publicPath); + expect(container.innerHTML).toEqual(options.publicPath); }); it('passes props to app component', () => { renderCheckout(options); - expect(CheckoutApp) - .toHaveBeenCalledWith(options, {}); + expect(CheckoutApp).toHaveBeenCalledWith(options, {}); }); it('does not configure `whyDidYouRender` if not in development mode', () => { renderCheckout(options); - expect(require('@welldone-software/why-did-you-render')) - .not.toHaveBeenCalled(); + expect(require('@welldone-software/why-did-you-render')).not.toHaveBeenCalled(); process.env.NODE_ENV = 'development'; }); @@ -85,10 +81,9 @@ describe('renderCheckout()', () => { renderCheckout(options); - expect(require('@welldone-software/why-did-you-render')) - .toHaveBeenCalledWith(React, { - collapseGroups: true, - }); + expect(require('@welldone-software/why-did-you-render')).toHaveBeenCalledWith(React, { + collapseGroups: true, + }); process.env.NODE_ENV = env; }); diff --git a/packages/core/src/app/checkout/renderCheckout.tsx b/packages/core/src/app/checkout/renderCheckout.tsx index 5aab69500d..0984582388 100644 --- a/packages/core/src/app/checkout/renderCheckout.tsx +++ b/packages/core/src/app/checkout/renderCheckout.tsx @@ -30,11 +30,7 @@ export default function renderCheckout({ } ReactDOM.render( - , - document.getElementById(containerId) + , + document.getElementById(containerId), ); } diff --git a/packages/core/src/app/checkout/withCheckout.spec.tsx b/packages/core/src/app/checkout/withCheckout.spec.tsx index e0a4cd280a..c4aa19ff6e 100644 --- a/packages/core/src/app/checkout/withCheckout.spec.tsx +++ b/packages/core/src/app/checkout/withCheckout.spec.tsx @@ -1,95 +1,105 @@ -import { createCheckoutService, Checkout } from '@bigcommerce/checkout-sdk'; +import { Checkout, createCheckoutService } from '@bigcommerce/checkout-sdk'; import { mount } from 'enzyme'; import React, { Fragment } from 'react'; +import CheckoutProvider from './CheckoutProvider'; import { getCheckout as getCheckoutMock } from './checkouts.mock'; import withCheckout from './withCheckout'; -import CheckoutProvider from './CheckoutProvider'; describe('withCheckout()', () => { it('provides checkout state to child component', () => { - const withMockCheckout = withCheckout(({ checkoutState: { data: { getCheckout } } }) => ({ - checkout: getCheckout(), - })); + const withMockCheckout = withCheckout( + ({ + checkoutState: { + data: { getCheckout }, + }, + }) => ({ + checkout: getCheckout(), + }), + ); const Child = withMockCheckout(({ checkout }: { checkout: Checkout }) => ( - { checkout.id } + <>{checkout.id} )); const service = createCheckoutService(); - jest.spyOn(service.getState().data, 'getCheckout') - .mockReturnValue({ ...getCheckoutMock(), id: '123' }); + jest.spyOn(service.getState().data, 'getCheckout').mockReturnValue({ + ...getCheckoutMock(), + id: '123', + }); const component = mount( - + - + , ); - expect(component.text()) - .toEqual('123'); + expect(component.text()).toBe('123'); }); it('provides checkout service to child component', () => { const withMockCheckout = withCheckout(({ checkoutService }) => ({ - loadCheckout: () => { checkoutService.loadCheckout(); }, + loadCheckout: () => { + checkoutService.loadCheckout(); + }, })); const Child = withMockCheckout(({ loadCheckout }: { loadCheckout(): void }) => ( - + )); const service = createCheckoutService(); - jest.spyOn(service, 'loadCheckout') - .mockReturnValue(Promise.resolve(service.getState())); + jest.spyOn(service, 'loadCheckout').mockReturnValue(Promise.resolve(service.getState())); const component = mount( - + - + , ); component.find('button').simulate('click'); - expect(service.loadCheckout) - .toHaveBeenCalled(); + expect(service.loadCheckout).toHaveBeenCalled(); }); it('does not update child if mapped props have not changed', () => { - const withMockCheckout = withCheckout(({ checkoutState: { data: { getCheckout } } }) => ({ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - grandTotal: getCheckout()!.grandTotal, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - subtotal: getCheckout()!.subtotal, - })); + const withMockCheckout = withCheckout( + ({ + checkoutState: { + data: { getCheckout }, + }, + }) => ({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + grandTotal: getCheckout()!.grandTotal, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + subtotal: getCheckout()!.subtotal, + }), + ); const OriginalChild = jest.fn((props: { grandTotal: number; subtotal: number }) => ( - - { props.grandTotal } - { props.subtotal } - + <> + {props.grandTotal} + {props.subtotal} + )); const Child = withMockCheckout(OriginalChild); const service = createCheckoutService(); - jest.spyOn(service.getState().data, 'getCheckout') - .mockReturnValue(getCheckoutMock()); + jest.spyOn(service.getState().data, 'getCheckout').mockReturnValue(getCheckoutMock()); const component = mount( - + - + , ); - expect(OriginalChild) - .toHaveBeenCalledTimes(1); + expect(OriginalChild).toHaveBeenCalledTimes(1); component.update(); - expect(OriginalChild) - .toHaveBeenCalledTimes(1); + expect(OriginalChild).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/core/src/app/checkout/withCheckout.tsx b/packages/core/src/app/checkout/withCheckout.tsx index ebce236243..53011c7aa3 100644 --- a/packages/core/src/app/checkout/withCheckout.tsx +++ b/packages/core/src/app/checkout/withCheckout.tsx @@ -4,6 +4,8 @@ import CheckoutContext, { CheckoutContextProps } from './CheckoutContext'; export type WithCheckoutProps = CheckoutContextProps; -const withCheckout = createMappableInjectHoc(CheckoutContext, { displayNamePrefix: 'WithCheckout' }); +const withCheckout = createMappableInjectHoc(CheckoutContext, { + displayNamePrefix: 'WithCheckout', +}); export default withCheckout; diff --git a/packages/core/src/app/common/bundler/configurePublicPath.spec.ts b/packages/core/src/app/common/bundler/configurePublicPath.spec.ts index 74b76f832f..9e35e8ceea 100644 --- a/packages/core/src/app/common/bundler/configurePublicPath.spec.ts +++ b/packages/core/src/app/common/bundler/configurePublicPath.spec.ts @@ -3,9 +3,7 @@ import configurePublicPath from './configurePublicPath'; // `document.currentScript` is a readonly property that can only be read at the // top level (outside of any function). Therefore, I need this workaround in // order to mock it. -jest.mock('./getCurrentScriptPath', () => - () => 'https://helloworld.com/assets/app.js' -); +jest.mock('./getCurrentScriptPath', () => () => 'https://helloworld.com/assets/app.js'); describe('configurePublicPath()', () => { const initialValue = (global as any).__webpack_public_path__; @@ -17,21 +15,18 @@ describe('configurePublicPath()', () => { it('sets public path for Webpack if path is provided', () => { configurePublicPath('https://foobar.com/assets/'); - expect((global as any).__webpack_public_path__) - .toEqual('https://foobar.com/assets/'); + expect((global as any).__webpack_public_path__).toBe('https://foobar.com/assets/'); }); it('adds trailing slash if it is not included in provided path', () => { configurePublicPath('https://foobar.com/assets'); - expect((global as any).__webpack_public_path__) - .toEqual('https://foobar.com/assets/'); + expect((global as any).__webpack_public_path__).toBe('https://foobar.com/assets/'); }); it('sets public path for Webpack using current script path if path is not provided', () => { configurePublicPath(); - expect((global as any).__webpack_public_path__) - .toEqual('https://helloworld.com/assets/'); + expect((global as any).__webpack_public_path__).toBe('https://helloworld.com/assets/'); }); }); diff --git a/packages/core/src/app/common/bundler/configurePublicPath.ts b/packages/core/src/app/common/bundler/configurePublicPath.ts index ef509c338e..7819bb889e 100644 --- a/packages/core/src/app/common/bundler/configurePublicPath.ts +++ b/packages/core/src/app/common/bundler/configurePublicPath.ts @@ -1,13 +1,13 @@ import getCurrentScriptPath from './getCurrentScriptPath'; -export default function configurePublicPath( - publicPath?: string -): string { +export default function configurePublicPath(publicPath?: string): string { if (!publicPath) { const scriptPath = getCurrentScriptPath(); if (!scriptPath) { - throw new Error('Unable to configure the public path of the application because it is not specified and it cannot be inferred using the path of the current script.'); + throw new Error( + 'Unable to configure the public path of the application because it is not specified and it cannot be inferred using the path of the current script.', + ); } __webpack_public_path__ = `${scriptPath.split('/').slice(0, -1).join('/')}/`; diff --git a/packages/core/src/app/common/bundler/getCurrentScriptPath.ts b/packages/core/src/app/common/bundler/getCurrentScriptPath.ts index dc8872eb83..f6cb9d1d6e 100644 --- a/packages/core/src/app/common/bundler/getCurrentScriptPath.ts +++ b/packages/core/src/app/common/bundler/getCurrentScriptPath.ts @@ -1,8 +1,6 @@ // `document.currentScript` can only be called at the global level as it only // holds a reference to the script when it is initially processed. -const path = document.currentScript ? - (document.currentScript as HTMLScriptElement).src : - undefined; +const path = document.currentScript ? (document.currentScript as HTMLScriptElement).src : undefined; export default function getCurrentScriptPath(): string | undefined { return path; diff --git a/packages/core/src/app/common/dom/appendStylesheet.spec.ts b/packages/core/src/app/common/dom/appendStylesheet.spec.ts index 098df63cad..e639579524 100644 --- a/packages/core/src/app/common/dom/appendStylesheet.spec.ts +++ b/packages/core/src/app/common/dom/appendStylesheet.spec.ts @@ -7,13 +7,12 @@ describe('appendStylesheet', () => { 'p { font-size: 20px; }', ]); - expect(style.parentElement) - .toEqual(document.head); + expect(style.parentElement).toEqual(document.head); - expect((style.sheet as CSSStyleSheet).cssRules[0].cssText) - .toEqual('body {background-color: rgb(20, 20, 20);}'); + expect((style.sheet as CSSStyleSheet).cssRules[0].cssText).toBe( + 'body {background-color: rgb(20, 20, 20);}', + ); - expect((style.sheet as CSSStyleSheet).cssRules[1].cssText) - .toEqual('p {font-size: 20px;}'); + expect((style.sheet as CSSStyleSheet).cssRules[1].cssText).toBe('p {font-size: 20px;}'); }); }); diff --git a/packages/core/src/app/common/dom/getAppliedStyles.spec.ts b/packages/core/src/app/common/dom/getAppliedStyles.spec.ts index e46d3d899b..1ab02a010c 100644 --- a/packages/core/src/app/common/dom/getAppliedStyles.spec.ts +++ b/packages/core/src/app/common/dom/getAppliedStyles.spec.ts @@ -38,17 +38,15 @@ describe('getAppliedStyles', () => { }); it('returns selected properties as map object', () => { - expect(getAppliedStyles(element, ['backgroundColor', 'fontSize'])) - .toEqual({ - backgroundColor: 'rgb(255, 0, 0)', - fontSize: '20px', - }); + expect(getAppliedStyles(element, ['backgroundColor', 'fontSize'])).toEqual({ + backgroundColor: 'rgb(255, 0, 0)', + fontSize: '20px', + }); }); it('returns inherited styles', () => { - expect(getAppliedStyles(element, ['color'])) - .toEqual({ - color: 'rgb(0, 0, 255)', - }); + expect(getAppliedStyles(element, ['color'])).toEqual({ + color: 'rgb(0, 0, 255)', + }); }); }); diff --git a/packages/core/src/app/common/dom/getAppliedStyles.ts b/packages/core/src/app/common/dom/getAppliedStyles.ts index 3d120f53d3..8db890abf3 100644 --- a/packages/core/src/app/common/dom/getAppliedStyles.ts +++ b/packages/core/src/app/common/dom/getAppliedStyles.ts @@ -2,12 +2,15 @@ import { kebabCase } from 'lodash'; export default function getAppliedStyles( element: HTMLElement, - properties: string[] + properties: string[], ): { [key: string]: string } { const declaration = window.getComputedStyle(element); - return properties.reduce((result, propertyName) => ({ - ...result, - [propertyName]: declaration.getPropertyValue(kebabCase(propertyName)), - }), {}); + return properties.reduce( + (result, propertyName) => ({ + ...result, + [propertyName]: declaration.getPropertyValue(kebabCase(propertyName)), + }), + {}, + ); } diff --git a/packages/core/src/app/common/dom/preventDefault.ts b/packages/core/src/app/common/dom/preventDefault.ts index 76ec9b796d..8f1b4001d3 100644 --- a/packages/core/src/app/common/dom/preventDefault.ts +++ b/packages/core/src/app/common/dom/preventDefault.ts @@ -1,9 +1,10 @@ import { SyntheticEvent } from 'react'; -export default function preventDefault any, TEvent extends SyntheticEvent>( - fn?: TFunc -): (event: TEvent) => void { - return event => { +export default function preventDefault< + TFunc extends (event: TEvent, ...args: any[]) => any, + TEvent extends SyntheticEvent, +>(fn?: TFunc): (event: TEvent) => void { + return (event) => { event.preventDefault(); if (fn) { diff --git a/packages/core/src/app/common/dom/stopPropagation.ts b/packages/core/src/app/common/dom/stopPropagation.ts index aa9c1ff25c..dbe3086497 100644 --- a/packages/core/src/app/common/dom/stopPropagation.ts +++ b/packages/core/src/app/common/dom/stopPropagation.ts @@ -1,9 +1,10 @@ import { SyntheticEvent } from 'react'; -export default function stopPropagation any, TEvent extends SyntheticEvent>( - fn?: TFunc -): (event: TEvent) => void { - return event => { +export default function stopPropagation< + TFunc extends (event: TEvent, ...args: any[]) => any, + TEvent extends SyntheticEvent, +>(fn?: TFunc): (event: TEvent) => void { + return (event) => { event.stopPropagation(); if (fn) { diff --git a/packages/core/src/app/common/dom/toCssRule.spec.ts b/packages/core/src/app/common/dom/toCssRule.spec.ts index bfdf5d0375..7ee746ca7d 100644 --- a/packages/core/src/app/common/dom/toCssRule.spec.ts +++ b/packages/core/src/app/common/dom/toCssRule.spec.ts @@ -4,15 +4,15 @@ describe('toCSSRule()', () => { it('converts to CSS rule', () => { const styles = { backgroundColor: '#fff', color: '#000' }; - expect(toCSSRule('.foobar', styles)) - .toEqual('.foobar {background-color: #fff; color: #000;}'); + expect(toCSSRule('.foobar', styles)).toBe('.foobar {background-color: #fff; color: #000;}'); }); it('merges CSS styles', () => { const stylesA = { backgroundColor: '#fff', color: '#000' }; const stylesB = { color: '#333', padding: '10px' }; - expect(toCSSRule('.foobar', stylesA, stylesB)) - .toEqual('.foobar {background-color: #fff; color: #333; padding: 10px;}'); + expect(toCSSRule('.foobar', stylesA, stylesB)).toBe( + '.foobar {background-color: #fff; color: #333; padding: 10px;}', + ); }); }); diff --git a/packages/core/src/app/common/dom/toCssRule.ts b/packages/core/src/app/common/dom/toCssRule.ts index 35fe6f8ff3..01937b2e95 100644 --- a/packages/core/src/app/common/dom/toCssRule.ts +++ b/packages/core/src/app/common/dom/toCssRule.ts @@ -1,9 +1,13 @@ import { assign, kebabCase, map, pickBy } from 'lodash'; -export default function toCSSRule(selector: string, ...styles: Array<{ [key: string]: any } | undefined>): string { +export default function toCSSRule( + selector: string, + ...styles: Array<{ [key: string]: any } | undefined> +): string { const mergedStyles = assign({}, ...styles); - const props = map(pickBy(mergedStyles, value => typeof value === 'string'), (value, key) => - `${kebabCase(key)}: ${value};` + const props = map( + pickBy(mergedStyles, (value) => typeof value === 'string'), + (value, key) => `${kebabCase(key)}: ${value};`, ).join(' '); return `${selector} {${props}}`; diff --git a/packages/core/src/app/common/error/ConsoleErrorLogger.spec.ts b/packages/core/src/app/common/error/ConsoleErrorLogger.spec.ts index 94f7fed58c..3009ef8e43 100644 --- a/packages/core/src/app/common/error/ConsoleErrorLogger.spec.ts +++ b/packages/core/src/app/common/error/ConsoleErrorLogger.spec.ts @@ -19,8 +19,7 @@ describe('ConsoleErrorLogger', () => { logger.log(error, tags); - expect(mockConsole.error) - .toHaveBeenCalledWith(error, tags, undefined); + expect(mockConsole.error).toHaveBeenCalledWith(error, tags, undefined); }); it('logs error as warning to console', () => { @@ -30,8 +29,7 @@ describe('ConsoleErrorLogger', () => { logger.log(error, tags, ErrorLevelType.Warning); - expect(mockConsole.warn) - .toHaveBeenCalledWith(error, tags, undefined); + expect(mockConsole.warn).toHaveBeenCalledWith(error, tags, undefined); }); it('logs error as info to console', () => { @@ -41,7 +39,6 @@ describe('ConsoleErrorLogger', () => { logger.log(error, tags, ErrorLevelType.Info); - expect(mockConsole.info) - .toHaveBeenCalledWith(error, tags, undefined); + expect(mockConsole.info).toHaveBeenCalledWith(error, tags, undefined); }); }); diff --git a/packages/core/src/app/common/error/ConsoleErrorLogger.ts b/packages/core/src/app/common/error/ConsoleErrorLogger.ts index a2f3ee7a02..e625eaadd5 100644 --- a/packages/core/src/app/common/error/ConsoleErrorLogger.ts +++ b/packages/core/src/app/common/error/ConsoleErrorLogger.ts @@ -9,12 +9,8 @@ export interface ConsoleErrorLoggerOptions { export default class ConsoleErrorLogger implements ErrorLogger { private console: Console; - constructor( - options?: ConsoleErrorLoggerOptions - ) { - const { - console: customConsole = console, - } = options || {}; + constructor(options?: ConsoleErrorLoggerOptions) { + const { console: customConsole = console } = options || {}; this.console = customConsole; } @@ -23,20 +19,20 @@ export default class ConsoleErrorLogger implements ErrorLogger { error: Error, tags?: ErrorTags, level: ErrorLevelType = ErrorLevelType.Error, - meta?: ErrorMeta + meta?: ErrorMeta, ): void { switch (level) { - case ErrorLevelType.Error: - return this.console.error(error, tags, meta); + case ErrorLevelType.Error: + return this.console.error(error, tags, meta); - case ErrorLevelType.Info: - return this.console.info(error, tags, meta); + case ErrorLevelType.Info: + return this.console.info(error, tags, meta); - case ErrorLevelType.Warning: - return this.console.warn(error, tags, meta); + case ErrorLevelType.Warning: + return this.console.warn(error, tags, meta); - default: - return this.console.log(error, tags, meta); + default: + return this.console.log(error, tags, meta); } } } diff --git a/packages/core/src/app/common/error/CustomError.ts b/packages/core/src/app/common/error/CustomError.ts index d3aea48d95..9cafbf149d 100644 --- a/packages/core/src/app/common/error/CustomError.ts +++ b/packages/core/src/app/common/error/CustomError.ts @@ -2,7 +2,7 @@ export default class CustomError extends Error { static shouldReport: boolean; data: any; - title: any; + title: string; type: string; constructor({ @@ -21,9 +21,10 @@ export default class CustomError extends Error { if (typeof Error.captureStackTrace === 'function') { Error.captureStackTrace(this, CustomError); } else { - this.stack = (new Error()).stack; + this.stack = new Error().stack; } + this.data = data; this.message = message; this.name = name; @@ -38,7 +39,11 @@ export default class CustomError extends Error { name, defaultError, defaultTitle, - }: { name: string; defaultError: string; defaultTitle: string}): void { + }: { + name: string; + defaultError: string; + defaultTitle: string; + }): void { this.name = this.name || name; this.message = this.message || defaultError; this.title = this.title || defaultTitle; diff --git a/packages/core/src/app/common/error/ErrorBoundary.spec.tsx b/packages/core/src/app/common/error/ErrorBoundary.spec.tsx index b9f4a1b065..5a372e670e 100644 --- a/packages/core/src/app/common/error/ErrorBoundary.spec.tsx +++ b/packages/core/src/app/common/error/ErrorBoundary.spec.tsx @@ -7,13 +7,11 @@ import ErrorLogger from './ErrorLogger'; describe('ErrorBoundary', () => { beforeEach(() => { // Need to mock `console.error` because React calls it deliberately - jest.spyOn(console, 'error') - .mockImplementation(); + jest.spyOn(console, 'error').mockImplementation(); }); afterEach(() => { - jest.spyOn(console, 'error') - .mockRestore(); + jest.spyOn(console, 'error').mockRestore(); }); it('logs error if logger is provided', () => { @@ -22,16 +20,17 @@ describe('ErrorBoundary', () => { log: jest.fn(), }; - const Child: FunctionComponent = () => { throw error; }; + const Child: FunctionComponent = () => { + throw error; + }; mount( - + - + , ); - expect(logger.log) - .toHaveBeenCalledWith(error); + expect(logger.log).toHaveBeenCalledWith(error); }); it('does not log error if filter returns false', () => { @@ -40,54 +39,54 @@ describe('ErrorBoundary', () => { log: jest.fn(), }; - const Child: FunctionComponent = () => { throw error; }; + const Child: FunctionComponent = () => { + throw error; + }; const filterError = ({ name }: Error) => name === 'TypeError'; try { mount( - + - + , ); } catch (error) { - expect(logger.log) - .not.toHaveBeenCalledWith(error); + expect(logger.log).not.toHaveBeenCalledWith(error); } }); it('renders fallback component if provided', () => { - const Child: FunctionComponent = () => { throw new Error(); }; + const Child: FunctionComponent = () => { + throw new Error(); + }; const component = mount( - Something went wrong }> + Something went wrong}> - + , ); - expect(component.html()) - .toEqual('Something went wrong'); + expect(component.html()).toBe('Something went wrong'); }); it('does not render fallback component if filter returns false', () => { const error = new Error(); - const Child: FunctionComponent = () => { throw error; }; + const Child: FunctionComponent = () => { + throw error; + }; const filterError = ({ name }: Error) => name === 'TypeError'; try { mount( Something went wrong } - filter={ filterError } + fallback={Something went wrong} + filter={filterError} > - +
    , ); } catch (thrown) { - expect(thrown) - .toEqual(error); + expect(thrown).toEqual(error); } }); }); diff --git a/packages/core/src/app/common/error/ErrorBoundary.tsx b/packages/core/src/app/common/error/ErrorBoundary.tsx index 3b2b194630..86a5050310 100644 --- a/packages/core/src/app/common/error/ErrorBoundary.tsx +++ b/packages/core/src/app/common/error/ErrorBoundary.tsx @@ -21,10 +21,7 @@ class ErrorBoundary extends React.Component true, - logger, - } = this.props; + const { filter = () => true, logger } = this.props; if (!filter(error)) { throw error; @@ -36,16 +33,12 @@ class ErrorBoundary extends React.Component true, - } = this.props; + const { children, fallback, filter = () => true } = this.props; const { error } = this.state; if (error && filter(error)) { - return fallback ? fallback : null; + return fallback || null; } return children; diff --git a/packages/core/src/app/common/error/ErrorCode.spec.tsx b/packages/core/src/app/common/error/ErrorCode.spec.tsx index 5ecaec7fa0..0ed109836c 100644 --- a/packages/core/src/app/common/error/ErrorCode.spec.tsx +++ b/packages/core/src/app/common/error/ErrorCode.spec.tsx @@ -6,8 +6,7 @@ import ErrorCode from './ErrorCode'; describe('ErrorCode Component', () => { it('renders error code component with provided code', () => { - const tree = shallow( - ); + const tree = shallow(); expect(toJson(tree)).toMatchSnapshot(); }); diff --git a/packages/core/src/app/common/error/ErrorCode.tsx b/packages/core/src/app/common/error/ErrorCode.tsx index bcaf02b486..92a29799a5 100644 --- a/packages/core/src/app/common/error/ErrorCode.tsx +++ b/packages/core/src/app/common/error/ErrorCode.tsx @@ -1,4 +1,4 @@ -import React, { memo, FunctionComponent, ReactNode } from 'react'; +import React, { FunctionComponent, memo, ReactNode } from 'react'; import { TranslatedString } from '../../locale'; @@ -13,10 +13,9 @@ const ErrorCode: FunctionComponent = ({ code, label }) => { return (
    - { label ?? } - - { ' ' } - { code } + {label ?? } + {' '} + {code}
    ); }; diff --git a/packages/core/src/app/common/error/ErrorLogger.ts b/packages/core/src/app/common/error/ErrorLogger.ts index 1253e63f80..c923eed54c 100644 --- a/packages/core/src/app/common/error/ErrorLogger.ts +++ b/packages/core/src/app/common/error/ErrorLogger.ts @@ -9,12 +9,7 @@ export default interface ErrorLogger { * @param level The level of the log * @param meta Any extra meta data */ - log( - error: Error, - tags?: ErrorTags, - level?: ErrorLevelType, - meta?: ErrorMeta - ): void; + log(error: Error, tags?: ErrorTags, level?: ErrorLevelType, meta?: ErrorMeta): void; } export interface ErrorLoggerOptions { diff --git a/packages/core/src/app/common/error/ErrorModal.spec.tsx b/packages/core/src/app/common/error/ErrorModal.spec.tsx index 565be664e1..b786f0bfeb 100644 --- a/packages/core/src/app/common/error/ErrorModal.spec.tsx +++ b/packages/core/src/app/common/error/ErrorModal.spec.tsx @@ -18,11 +18,8 @@ describe('ErrorModal', () => { const onClose = jest.fn(); const ErrorModalContainer = (props: ErrorModalProps) => ( - - + + ); @@ -30,7 +27,7 @@ describe('ErrorModal', () => { error = new Error('Foo'); localeContext = createLocaleContext(getStoreConfig()); - errorModal = mount(); + errorModal = mount(); }); it('renders error modal', () => { @@ -41,12 +38,11 @@ describe('ErrorModal', () => { it('hides error modal if there is no error', () => { errorModal = mount(); - expect(errorModal.find(Modal).prop('isOpen')) - .toBeFalsy(); + expect(errorModal.find(Modal).prop('isOpen')).toBeFalsy(); }); it('renders error code', () => { - expect(errorModal.find(ErrorCode).length).toEqual(1); + expect(errorModal.find(ErrorCode)).toHaveLength(1); }); it('renders request ID if available', () => { @@ -55,30 +51,21 @@ describe('ErrorModal', () => { headers: { 'x-request-id': 'foobar' }, } as unknown as RequestError; - errorModal = mount(); + errorModal = mount(); - expect(errorModal.find(ErrorCode).text()) - .toEqual('Request ID: foobar'); + expect(errorModal.find(ErrorCode).text()).toBe('Request ID: foobar'); }); it('overrides error message', () => { - errorModal = mount(); + errorModal = mount(); - expect(errorModal.find('[data-test="modal-body"]').text()) - .toContain('Hello world'); + expect(errorModal.find('[data-test="modal-body"]').text()).toContain('Hello world'); }); it('overrides error title', () => { - errorModal = mount(); + errorModal = mount(); - expect(errorModal.find('[data-test="modal-heading"]').text()) - .toContain('Hello world'); + expect(errorModal.find('[data-test="modal-heading"]').text()).toContain('Hello world'); }); describe('when modal is closed', () => { @@ -87,10 +74,11 @@ describe('ErrorModal', () => { }); it('calls `onAfterClose` callback with event and error object', async () => { - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(onClose) - .toHaveBeenCalledWith(expect.objectContaining({ type: 'click' }), { error }); + expect(onClose).toHaveBeenCalledWith(expect.objectContaining({ type: 'click' }), { + error, + }); }); describe('when the error is updated', () => { @@ -110,12 +98,11 @@ describe('ErrorModal', () => { errorModal.setProps({ error: null }); errorModal.update(); - expect(errorModal.find(Modal).prop('isOpen')) - .toBeFalsy(); + expect(errorModal.find(Modal).prop('isOpen')).toBeFalsy(); }); it('does not render error code', () => { - expect(errorModal.find(ErrorCode).length).toEqual(0); + expect(errorModal.find(ErrorCode)).toHaveLength(0); }); }); }); diff --git a/packages/core/src/app/common/error/ErrorModal.tsx b/packages/core/src/app/common/error/ErrorModal.tsx index b61034635e..1875cef6c0 100644 --- a/packages/core/src/app/common/error/ErrorModal.tsx +++ b/packages/core/src/app/common/error/ErrorModal.tsx @@ -1,6 +1,6 @@ import { RequestError } from '@bigcommerce/checkout-sdk'; import { noop } from 'lodash'; -import React, { Fragment, PureComponent, ReactNode, SyntheticEvent } from 'react'; +import React, { PureComponent, ReactNode, SyntheticEvent } from 'react'; import { TranslatedString } from '../../locale'; import { Button, ButtonSize } from '../../ui/button'; @@ -8,9 +8,9 @@ import { IconError, IconSize } from '../../ui/icon'; import { Modal, ModalHeader } from '../../ui/modal'; import computeErrorCode from './computeErrorCode'; +import ErrorCode from './ErrorCode'; import isCustomError from './isCustomError'; import isRequestError from './isRequestError'; -import ErrorCode from './ErrorCode'; export interface ErrorModalProps { error?: Error | RequestError; @@ -35,74 +35,71 @@ export default class ErrorModal extends PureComponent { return ( - { this.renderBody() } + {this.renderBody()} ); } private renderHeader(): ReactNode { - const { - error, - title = error && isCustomError(error) && error.title, - } = this.props; + const { error, title = error && isCustomError(error) && error.title } = this.props; return ( - - { title || } + + + {title || } + ); } private renderBody(): ReactNode { - const { - error, - message = error && error.message, - } = this.props; + const { error, message = error && error.message } = this.props; return ( - - { message && } - -
    - { this.renderErrorCode() } -
    -
    + <> + {message && ( + + )} + +
    {this.renderErrorCode()}
    + ); } private renderFooter(): ReactNode { return ( - ); } private renderErrorCode(): ReactNode { - const { - error, - shouldShowErrorCode = true, - } = this.props; + const { error, shouldShowErrorCode = true } = this.props; if (!error || !shouldShowErrorCode) { return; } - - if (isRequestError(error) && error?.headers?.['x-request-id']) { - return } - />; + + if (isRequestError(error) && error.headers?.['x-request-id']) { + return ( + } + /> + ); } const errorCode = computeErrorCode(error); @@ -111,14 +108,11 @@ export default class ErrorModal extends PureComponent { return; } - return ; + return ; } - private handleOnRequestClose: (event: SyntheticEvent) => void = event => { - const { - error, - onClose = noop, - } = this.props; + private handleOnRequestClose: (event: SyntheticEvent) => void = (event) => { + const { error, onClose = noop } = this.props; if (error) { onClose(event.nativeEvent, { error }); diff --git a/packages/core/src/app/common/error/NoopErrorLogger.ts b/packages/core/src/app/common/error/NoopErrorLogger.ts index 9f5791635e..5b836f576c 100644 --- a/packages/core/src/app/common/error/NoopErrorLogger.ts +++ b/packages/core/src/app/common/error/NoopErrorLogger.ts @@ -1,7 +1,5 @@ import ErrorLogger from './ErrorLogger'; export default class NoopErrorLogger implements ErrorLogger { - log() { - return; - } + log() {} } diff --git a/packages/core/src/app/common/error/SentryErrorLogger.spec.ts b/packages/core/src/app/common/error/SentryErrorLogger.spec.ts index ed7d80a083..2a5b167fe7 100644 --- a/packages/core/src/app/common/error/SentryErrorLogger.spec.ts +++ b/packages/core/src/app/common/error/SentryErrorLogger.spec.ts @@ -1,4 +1,11 @@ -import { captureException, init, withScope, BrowserOptions, Integrations, Scope } from '@sentry/browser'; +import { + BrowserOptions, + captureException, + init, + Integrations, + Scope, + withScope, +} from '@sentry/browser'; import { RewriteFrames } from '@sentry/integrations'; import { Integration } from '@sentry/types'; @@ -37,30 +44,28 @@ describe('SentryErrorLogger', () => { }); it('does not log exception event if it is not raised by error', () => { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions new SentryErrorLogger(config); const clientOptions: BrowserOptions = (init as jest.Mock).mock.calls[0][0]; const event = { exception: { - values: [{ - stacktrace: { frames: [{ filename: 'js/app-123.js' }] }, - type: 'Error', - value: 'Unexpected error', - }], + values: [ + { + stacktrace: { frames: [{ filename: 'js/app-123.js' }] }, + type: 'Error', + value: 'Unexpected error', + }, + ], }, }; const hint = { originalException: 'Unexpected error' }; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(clientOptions.beforeSend!(event, hint)) - .toEqual(null); - expect(clientOptions.sampleRate) - .toStrictEqual(0.123); + expect(clientOptions.beforeSend!(event, hint)).toBeNull(); + expect(clientOptions.sampleRate).toBe(0.123); }); it('does not log exception event if it does not contain stacktrace', () => { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions new SentryErrorLogger(config); const clientOptions: BrowserOptions = (init as jest.Mock).mock.calls[0][0]; @@ -70,153 +75,151 @@ describe('SentryErrorLogger', () => { const hint = { originalException: new Error('Unexpected error') }; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(clientOptions.beforeSend!(event, hint)) - .toEqual(null); + expect(clientOptions.beforeSend!(event, hint)).toBeNull(); }); it('does not log exception event if all frames in stacktrace are missing filename', () => { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions new SentryErrorLogger(config); const clientOptions: BrowserOptions = (init as jest.Mock).mock.calls[0][0]; const event = { exception: { - values: [{ - stacktrace: { frames: [{ filename: '' }] }, - type: 'Error', - value: 'Unexpected error', - }], + values: [ + { + stacktrace: { frames: [{ filename: '' }] }, + type: 'Error', + value: 'Unexpected error', + }, + ], }, }; const hint = { originalException: new Error('Unexpected error') }; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(clientOptions.beforeSend!(event, hint)) - .toEqual(null); + expect(clientOptions.beforeSend!(event, hint)).toBeNull(); }); it('logs exception event if all frames in stacktrace reference app file', () => { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions new SentryErrorLogger(config); const clientOptions: BrowserOptions = (init as jest.Mock).mock.calls[0][0]; const event = { exception: { - values: [{ - stacktrace: { frames: [{ filename: 'app:///js/app-123.js' }, { filename: 'app:///js/app-456.js' }] }, - type: 'Error', - value: 'Unexpected error', - }], + values: [ + { + stacktrace: { + frames: [ + { filename: 'app:///js/app-123.js' }, + { filename: 'app:///js/app-456.js' }, + ], + }, + type: 'Error', + value: 'Unexpected error', + }, + ], }, }; const hint = { originalException: new Error('Unexpected error') }; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(clientOptions.beforeSend!(event, hint)) - .toEqual(event); + expect(clientOptions.beforeSend!(event, hint)).toEqual(event); }); it('configures client to rewrite filename of error frames', () => { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions new SentryErrorLogger(config, { publicPath: 'https://cdn.foo.bar' }); const clientOptions: BrowserOptions = (init as jest.Mock).mock.calls[0][0]; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const rewriteFrames = (clientOptions.integrations! as Integration[]).find(integration => ( - integration.name === 'RewriteFrames' - )) as RewriteFrames; + const rewriteFrames = (clientOptions.integrations! as Integration[]).find( + (integration) => integration.name === 'RewriteFrames', + ) as RewriteFrames; const output = rewriteFrames.process({ exception: { - values: [{ - stacktrace: { - frames: [ - { - colno: 1234, - filename: 'https://cdn.foo.bar/js/app-123.js', - function: 't.', - in_app: true, - lineno: 1, - }, - ], + values: [ + { + stacktrace: { + frames: [ + { + colno: 1234, + filename: 'https://cdn.foo.bar/js/app-123.js', + function: 't.', + in_app: true, + lineno: 1, + }, + ], + }, }, - }], - } + ], + }, }); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const frame = output.exception!.values![0].stacktrace!.frames![0]; - expect(frame) - .toEqual({ - ...frame, - filename: 'app:///js/app-123.js', - }); + expect(frame).toEqual({ + ...frame, + filename: 'app:///js/app-123.js', + }); }); it('configures client to ignore errors from polyfill and Sentry client', () => { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions new SentryErrorLogger(config); - expect(init) - .toHaveBeenCalledWith(expect.objectContaining({ - denyUrls: [ - 'polyfill~checkout', - 'sentry~checkout', - ], - })); + expect(init).toHaveBeenCalledWith( + expect.objectContaining({ + denyUrls: ['polyfill~checkout', 'sentry~checkout'], + }), + ); }); it('does not rewrite filename of error frames if it does not match with public path', () => { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions new SentryErrorLogger(config, { publicPath: 'https://cdn.foo.bar' }); const clientOptions: BrowserOptions = (init as jest.Mock).mock.calls[0][0]; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const rewriteFrames = (clientOptions.integrations! as Integration[]).find(integration => ( - integration.name === 'RewriteFrames' - )) as RewriteFrames; + const rewriteFrames = (clientOptions.integrations! as Integration[]).find( + (integration) => integration.name === 'RewriteFrames', + ) as RewriteFrames; const output = rewriteFrames.process({ exception: { - values: [{ - stacktrace: { - frames: [ - { - colno: 1234, - filename: 'https://cdn.hello.world/js/app-123.js', - function: 't.', - in_app: true, - lineno: 1, - }, - ], + values: [ + { + stacktrace: { + frames: [ + { + colno: 1234, + filename: 'https://cdn.hello.world/js/app-123.js', + function: 't.', + in_app: true, + lineno: 1, + }, + ], + }, }, - }], - } + ], + }, }); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const frame = output.exception!.values![0].stacktrace!.frames![0]; - expect(frame) - .toEqual({ - ...frame, - filename: 'https://cdn.hello.world/js/app-123.js', - }); + expect(frame).toEqual({ + ...frame, + filename: 'https://cdn.hello.world/js/app-123.js', + }); }); - it('disables global error handler', () => { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions new SentryErrorLogger(config); - expect(Integrations.GlobalHandlers) - .toHaveBeenCalledWith({ - onerror: false, - onunhandledrejection: true, - }); + expect(Integrations.GlobalHandlers).toHaveBeenCalledWith({ + onerror: false, + onunhandledrejection: true, + }); }); describe('#log()', () => { @@ -229,7 +232,7 @@ describe('SentryErrorLogger', () => { setFingerprint: jest.fn(), }; - (withScope as jest.Mock).mockImplementation(fn => fn(scope)); + (withScope as jest.Mock).mockImplementation((fn) => fn(scope)); }); it('logs error with provided error code, level and default fingerprint', () => { @@ -239,17 +242,13 @@ describe('SentryErrorLogger', () => { logger.log(error, tags, ErrorLevelType.Warning); - expect(scope.setLevel) - .toHaveBeenCalledWith('warning'); + expect(scope.setLevel).toHaveBeenCalledWith('warning'); - expect(scope.setTags) - .toHaveBeenCalledWith(tags); + expect(scope.setTags).toHaveBeenCalledWith(tags); - expect(scope.setFingerprint) - .toHaveBeenCalledWith(['{{ default }}']); + expect(scope.setFingerprint).toHaveBeenCalledWith(['{{ default }}']); - expect(captureException) - .toHaveBeenCalledWith(error); + expect(captureException).toHaveBeenCalledWith(error); }); it('logs error with default error code, level and specific fingerprint if level / code is not provided', () => { @@ -258,17 +257,13 @@ describe('SentryErrorLogger', () => { logger.log(error); - expect(scope.setLevel) - .toHaveBeenCalledWith('error'); + expect(scope.setLevel).toHaveBeenCalledWith('error'); - expect(scope.setTags) - .toHaveBeenCalledWith({ errorCode: computeErrorCode(error) }); + expect(scope.setTags).toHaveBeenCalledWith({ errorCode: computeErrorCode(error) }); - expect(scope.setFingerprint) - .toHaveBeenCalledWith(['{{ default }}']); + expect(scope.setFingerprint).toHaveBeenCalledWith(['{{ default }}']); - expect(captureException) - .toHaveBeenCalledWith(error); + expect(captureException).toHaveBeenCalledWith(error); }); it('maps to error level enum recognized by Sentry', () => { @@ -279,21 +274,17 @@ describe('SentryErrorLogger', () => { logger.log(error, undefined, ErrorLevelType.Warning); logger.log(error, undefined, ErrorLevelType.Info); - expect(scope.setLevel) - .toHaveBeenNthCalledWith(1, SeverityLevelEnum.ERROR); + expect(scope.setLevel).toHaveBeenNthCalledWith(1, SeverityLevelEnum.ERROR); - expect(scope.setLevel) - .toHaveBeenNthCalledWith(2, SeverityLevelEnum.WARNING); + expect(scope.setLevel).toHaveBeenNthCalledWith(2, SeverityLevelEnum.WARNING); - expect(scope.setLevel) - .toHaveBeenNthCalledWith(3, SeverityLevelEnum.INFO); + expect(scope.setLevel).toHaveBeenNthCalledWith(3, SeverityLevelEnum.INFO); }); it('logs error in console if console logger is provided', () => { const consoleLogger = new ConsoleErrorLogger(); - jest.spyOn(consoleLogger, 'log') - .mockImplementation(); + jest.spyOn(consoleLogger, 'log').mockImplementation(); const logger = new SentryErrorLogger(config, { consoleLogger }); const error = new Error('Testing 123'); @@ -302,8 +293,7 @@ describe('SentryErrorLogger', () => { logger.log(error, tags, level); - expect(consoleLogger.log) - .toHaveBeenCalledWith(error, tags, level); + expect(consoleLogger.log).toHaveBeenCalledWith(error, tags, level); }); }); }); diff --git a/packages/core/src/app/common/error/SentryErrorLogger.ts b/packages/core/src/app/common/error/SentryErrorLogger.ts index c02b5d4946..de0c8ddb77 100644 --- a/packages/core/src/app/common/error/SentryErrorLogger.ts +++ b/packages/core/src/app/common/error/SentryErrorLogger.ts @@ -1,4 +1,13 @@ -import { captureException, init, withScope, BrowserOptions, Event, Integrations, SeverityLevel, StackFrame } from '@sentry/browser'; +import { + BrowserOptions, + captureException, + Event, + init, + Integrations, + SeverityLevel, + StackFrame, + withScope, +} from '@sentry/browser'; import { RewriteFrames } from '@sentry/integrations'; import { EventHint, Exception } from '@sentry/types'; @@ -27,14 +36,8 @@ export default class SentryErrorLogger implements ErrorLogger { private consoleLogger: ErrorLogger; private publicPath: string; - constructor( - config: BrowserOptions, - options?: SentryErrorLoggerOptions - ) { - const { - consoleLogger = new NoopErrorLogger(), - publicPath = '', - } = options || {}; + constructor(config: BrowserOptions, options?: SentryErrorLoggerOptions) { + const { consoleLogger = new NoopErrorLogger(), publicPath = '' } = options || {}; this.consoleLogger = consoleLogger; this.publicPath = publicPath; @@ -42,11 +45,7 @@ export default class SentryErrorLogger implements ErrorLogger { init({ sampleRate: SAMPLE_RATE, beforeSend: this.handleBeforeSend, - denyUrls: [ - ...(config.denyUrls || []), - 'polyfill~checkout', - 'sentry~checkout', - ], + denyUrls: [...(config.denyUrls || []), 'polyfill~checkout', 'sentry~checkout'], integrations: [ new Integrations.GlobalHandlers({ onerror: false, @@ -64,11 +63,11 @@ export default class SentryErrorLogger implements ErrorLogger { error: Error, tags?: ErrorTags, level: ErrorLevelType = ErrorLevelType.Error, - payload?: ErrorMeta + payload?: ErrorMeta, ): void { this.consoleLogger.log(error, tags, level); - withScope(scope => { + withScope((scope) => { const { errorCode = computeErrorCode(error) } = tags || {}; if (errorCode) { @@ -112,29 +111,37 @@ export default class SentryErrorLogger implements ErrorLogger { * sufficient for us because some stores have customisation code built on top of our code, resulting in a stacktrace * whose topmost frame is ours but frames below it are not. */ - private shouldReportExceptions(exceptions: Exception[], originalException: Error | string | null): boolean { + private shouldReportExceptions( + exceptions: Exception[], + originalException: Error | string | null, + ): boolean { // Ignore exceptions that are not an instance of Error because they are most likely not thrown by our own code, // as we have a lint rule that prevents us from doing so. Although these exceptions don't actually have a // stacktrace, meaning that the condition below should theoretically cover the scenario, but we still need this // condition because Sentry client creates a "synthentic" stacktrace for them using the information it has. - if (!exceptions?.length || !(originalException instanceof Error)) { + if (!exceptions.length || !(originalException instanceof Error)) { return false; } - return exceptions.every(exception => { + return exceptions.every((exception) => { if (!exception.stacktrace?.frames?.length) { return false; } - return exception.stacktrace?.frames?.every(frame => - frame.filename?.startsWith(FILENAME_PREFIX) + return exception.stacktrace.frames.every((frame) => + frame.filename?.startsWith(FILENAME_PREFIX), ); }); } private handleBeforeSend: (event: Event, hint?: EventHint) => Event | null = (event, hint) => { if (event.exception) { - if (!this.shouldReportExceptions(event.exception.values ?? [], hint?.originalException ?? null)) { + if ( + !this.shouldReportExceptions( + event.exception.values ?? [], + hint?.originalException ?? null, + ) + ) { return null; } @@ -144,7 +151,7 @@ export default class SentryErrorLogger implements ErrorLogger { return event; }; - private handleRewriteFrame: (frame: StackFrame) => StackFrame = frame => { + private handleRewriteFrame: (frame: StackFrame) => StackFrame = (frame) => { if (this.publicPath && frame.filename) { // We want to remove the base path of the filename, otherwise we // will need to specify it when we upload the sourcemaps so that the diff --git a/packages/core/src/app/common/error/computeErrorCode.spec.ts b/packages/core/src/app/common/error/computeErrorCode.spec.ts index df10425dd5..294de75552 100644 --- a/packages/core/src/app/common/error/computeErrorCode.spec.ts +++ b/packages/core/src/app/common/error/computeErrorCode.spec.ts @@ -2,21 +2,20 @@ import computeErrorCode from './computeErrorCode'; describe('computerErrorCode()', () => { it('returns error code by hashing input', () => { - expect(computeErrorCode(new Error('Testing 123'))) - .toEqual('F9516497646215C5AF995E0B797FE138FA29ECBA'); + expect(computeErrorCode(new Error('Testing 123'))).toBe( + 'F9516497646215C5AF995E0B797FE138FA29ECBA', + ); - expect(computeErrorCode('Testing 123')) - .toEqual('2387ED48D743D74DD80D8D363692817FC8A36AAA'); + expect(computeErrorCode('Testing 123')).toBe('2387ED48D743D74DD80D8D363692817FC8A36AAA'); - expect(computeErrorCode({ error: 'Testing 123' })) - .toEqual('57DD26761A556285AEC130DC14E581CB5D411329'); + expect(computeErrorCode({ error: 'Testing 123' })).toBe( + '57DD26761A556285AEC130DC14E581CB5D411329', + ); }); it('returns empty string if unable to compute error code', () => { - expect(computeErrorCode(new Event('click'))) - .toBeUndefined(); + expect(computeErrorCode(new Event('click'))).toBeUndefined(); - expect(computeErrorCode(new DOMException('Testing 123'))) - .toBeUndefined(); + expect(computeErrorCode(new DOMException('Testing 123'))).toBeUndefined(); }); }); diff --git a/packages/core/src/app/common/error/computeErrorCode.ts b/packages/core/src/app/common/error/computeErrorCode.ts index 1cfaa3d004..890490fe94 100644 --- a/packages/core/src/app/common/error/computeErrorCode.ts +++ b/packages/core/src/app/common/error/computeErrorCode.ts @@ -3,7 +3,5 @@ import HashStatic from 'object-hash'; export default function computeErrorCode(value: any): string | undefined { try { return HashStatic(value).toUpperCase(); - } catch (error) { - return; - } + } catch (error) {} } diff --git a/packages/core/src/app/common/error/createCustomErrorType.spec.ts b/packages/core/src/app/common/error/createCustomErrorType.spec.ts index 3395e7470d..66a3883f45 100644 --- a/packages/core/src/app/common/error/createCustomErrorType.spec.ts +++ b/packages/core/src/app/common/error/createCustomErrorType.spec.ts @@ -31,6 +31,6 @@ describe('createCustomError()', () => { const MyError = createCustomErrorType({ name: 'Foobar' }); const myError = new MyError(); - expect(myError.type).toEqual('custom'); + expect(myError.type).toBe('custom'); }); }); diff --git a/packages/core/src/app/common/error/createErrorLogger.spec.ts b/packages/core/src/app/common/error/createErrorLogger.spec.ts index a4b6c524d5..90df03256a 100644 --- a/packages/core/src/app/common/error/createErrorLogger.spec.ts +++ b/packages/core/src/app/common/error/createErrorLogger.spec.ts @@ -4,16 +4,16 @@ import SentryErrorLogger from './SentryErrorLogger'; describe('createErrorLogger()', () => { it('returns instance of noop logger', () => { - expect(createErrorLogger()) - .toBeInstanceOf(NoopErrorLogger); + expect(createErrorLogger()).toBeInstanceOf(NoopErrorLogger); }); it('returns instance of Sentry logger if Sentry config is provided', () => { - expect(createErrorLogger({ - sentry: { - dsn: 'https://abc@sentry.io/123', - }, - })) - .toBeInstanceOf(SentryErrorLogger); + expect( + createErrorLogger({ + sentry: { + dsn: 'https://abc@sentry.io/123', + }, + }), + ).toBeInstanceOf(SentryErrorLogger); }); }); diff --git a/packages/core/src/app/common/error/createErrorLogger.ts b/packages/core/src/app/common/error/createErrorLogger.ts index a085823fd3..291303a07a 100644 --- a/packages/core/src/app/common/error/createErrorLogger.ts +++ b/packages/core/src/app/common/error/createErrorLogger.ts @@ -5,13 +5,13 @@ import SentryErrorLogger from './SentryErrorLogger'; export default function createErrorLogger( serviceConfig?: ErrorLoggerServiceConfig, - options?: ErrorLoggerOptions + options?: ErrorLoggerOptions, ): ErrorLogger { if (serviceConfig && serviceConfig.sentry) { - return new SentryErrorLogger( - serviceConfig.sentry, - { ...options, consoleLogger: new ConsoleErrorLogger(options) } - ); + return new SentryErrorLogger(serviceConfig.sentry, { + ...options, + consoleLogger: new ConsoleErrorLogger(options), + }); } if (process.env.NODE_ENV === 'test') { diff --git a/packages/core/src/app/common/error/isCustomError.ts b/packages/core/src/app/common/error/isCustomError.ts index 597b9daec6..396d529701 100644 --- a/packages/core/src/app/common/error/isCustomError.ts +++ b/packages/core/src/app/common/error/isCustomError.ts @@ -3,7 +3,9 @@ import CustomError from './CustomError'; export default function isCustomError(error: Error): error is CustomError { const customError = error as CustomError; - return typeof customError.title !== 'undefined' && + return ( + typeof customError.title !== 'undefined' && typeof customError.data !== 'undefined' && - typeof customError.type !== 'undefined'; + typeof customError.type !== 'undefined' + ); } diff --git a/packages/core/src/app/common/error/isErrorWithType.ts b/packages/core/src/app/common/error/isErrorWithType.ts index 128706b12e..fefea06331 100644 --- a/packages/core/src/app/common/error/isErrorWithType.ts +++ b/packages/core/src/app/common/error/isErrorWithType.ts @@ -5,14 +5,18 @@ interface ErrorWithType extends RequestError { } // eslint-disable-next-line @typescript-eslint/ban-types -function hasOwnProperty - (obj: X, key: Y): obj is X & Record { +function hasOwnProperty( + obj: X, + key: Y, +): obj is X & Record { return Object.prototype.hasOwnProperty.call(obj, key); } export default function isErrorWithType(error: unknown): error is ErrorWithType { - return typeof error === 'object' - && error !== null - && hasOwnProperty(error, 'type') - && typeof error.type === 'string'; + return ( + typeof error === 'object' && + error !== null && + hasOwnProperty(error, 'type') && + typeof error.type === 'string' + ); } diff --git a/packages/core/src/app/common/form/connectFormik.spec.tsx b/packages/core/src/app/common/form/connectFormik.spec.tsx index 71e002ca87..3ed7c1d133 100644 --- a/packages/core/src/app/common/form/connectFormik.spec.tsx +++ b/packages/core/src/app/common/form/connectFormik.spec.tsx @@ -6,7 +6,6 @@ import React, { FunctionComponent } from 'react'; import connectFormik from './connectFormik'; import ConnectFormikProps from './ConnectFormikProps'; -/* eslint-disable react/jsx-no-bind */ describe('connectFormik()', () => { it('only re-renders connected component if Formik props have changed', () => { const TestComponent: FunctionComponent = jest.fn(() =>
    ); @@ -15,63 +14,64 @@ describe('connectFormik()', () => { mount( { + initialValues={{ message: 'foobar' }} + onSubmit={jest.fn()} + render={(formik) => { setFieldValue = formik.setFieldValue; - return <> - - - ; - } } - /> + return ( + <> + + + + ); + }} + />, ); - expect(TestComponent) - .toHaveBeenCalledTimes(1); + expect(TestComponent).toHaveBeenCalledTimes(1); // Setting the same value as the initial value setFieldValue('message', 'foobar'); - expect(TestComponent) - .toHaveBeenCalledTimes(1); + expect(TestComponent).toHaveBeenCalledTimes(1); // Setting a value different to the initial value setFieldValue('message', 'hello'); - expect(TestComponent) - .toHaveBeenCalledTimes(2); + expect(TestComponent).toHaveBeenCalledTimes(2); }); it('also re-renders connected component if non-Formik props have changed', () => { - const TestComponent: FunctionComponent<{ count: number } & ConnectFormikProps<{ message: string }>> = jest.fn(() =>
    ); + const TestComponent: FunctionComponent< + { count: number } & ConnectFormikProps<{ message: string }> + > = jest.fn(() =>
    ); const ConnectedTestComponent = connectFormik(TestComponent); const initialValues = { message: 'foobar' }; const handleSubmit = jest.fn(); - const Container: FunctionComponent<{ count: number }> = props => ( + const Container: FunctionComponent<{ count: number }> = (props) => ( { - return <> - - - ; - } } + initialValues={initialValues} + onSubmit={handleSubmit} + render={() => { + return ( + <> + + + + ); + }} /> ); - const container = mount(); + const container = mount(); - expect(TestComponent) - .toHaveBeenCalledTimes(1); + expect(TestComponent).toHaveBeenCalledTimes(1); // Changing the value of `count`, which is passed to // `ConnectedTestComponent`, should re-render that component. container.setProps({ count: 2 }); - expect(TestComponent) - .toHaveBeenCalledTimes(2); + expect(TestComponent).toHaveBeenCalledTimes(2); }); }); diff --git a/packages/core/src/app/common/form/connectFormik.tsx b/packages/core/src/app/common/form/connectFormik.tsx index f529156d40..23d2ea0df0 100644 --- a/packages/core/src/app/common/form/connectFormik.tsx +++ b/packages/core/src/app/common/form/connectFormik.tsx @@ -1,25 +1,25 @@ import { connect } from 'formik'; -import React, { memo, ComponentType, FunctionComponent } from 'react'; +import React, { ComponentType, FunctionComponent, memo } from 'react'; import shallowEqual from 'shallowequal'; import ConnectFormikProps from './ConnectFormikProps'; -export default function connectFormik< - TProps extends ConnectFormikProps, - TValues = any ->( - OriginalComponent: ComponentType +export default function connectFormik, TValues = any>( + OriginalComponent: ComponentType, ): ComponentType>> { const InnerComponent: FunctionComponent = memo( - props => , - ({ formik: prevFormik, ...prevProps }, { formik: nextFormik, ...nextProps }) => ( - shallowEqual(prevFormik, nextFormik) && shallowEqual(prevProps, nextProps) - ) + (props) => , + ({ formik: prevFormik, ...prevProps }, { formik: nextFormik, ...nextProps }) => + shallowEqual(prevFormik, nextFormik) && shallowEqual(prevProps, nextProps), ); - const DecoratedComponent = connect(InnerComponent) as ComponentType>>; + const DecoratedComponent = connect(InnerComponent) as ComponentType< + Omit> + >; - DecoratedComponent.displayName = `ConnectFormik(${OriginalComponent.displayName || OriginalComponent.name})`; + DecoratedComponent.displayName = `ConnectFormik(${ + OriginalComponent.displayName || OriginalComponent.name + })`; return DecoratedComponent; } diff --git a/packages/core/src/app/common/hoc/InjectHoc.ts b/packages/core/src/app/common/hoc/InjectHoc.ts index c0cf2fd47c..39c41013f7 100644 --- a/packages/core/src/app/common/hoc/InjectHoc.ts +++ b/packages/core/src/app/common/hoc/InjectHoc.ts @@ -3,13 +3,17 @@ import { Omit } from 'utility-types'; export type MatchedProps = { [P in keyof TProps]: P extends keyof TInjectedProps - ? (TInjectedProps[P] extends TProps[P] ? TProps[P] : never) + ? TInjectedProps[P] extends TProps[P] + ? TProps[P] + : never : TProps[P]; }; // eslint-disable-next-line @typescript-eslint/ban-types -type InjectHoc = & TOwnProps>( - OriginalComponent: ComponentType +type InjectHoc = < + TProps extends MatchedProps & TOwnProps, +>( + OriginalComponent: ComponentType, ) => ComponentType>; export default InjectHoc; diff --git a/packages/core/src/app/common/hoc/MappableInjectHoc.tsx b/packages/core/src/app/common/hoc/MappableInjectHoc.tsx index cbf6729f7f..d2420a8115 100644 --- a/packages/core/src/app/common/hoc/MappableInjectHoc.tsx +++ b/packages/core/src/app/common/hoc/MappableInjectHoc.tsx @@ -2,16 +2,19 @@ import InjectHoc from './InjectHoc'; export type MapToProps = ( context: TContextProps, - props: TOwnProps + props: TOwnProps, ) => TMappedProps | null; -export type MapToPropsFactory = () => MapToProps; +export type MapToPropsFactory = () => MapToProps< + TContextProps, + TMappedProps, + TOwnProps +>; type MappableInjectHoc = ( - mapToProps: ( - MapToProps | - MapToPropsFactory - ) + mapToProps: + | MapToProps + | MapToPropsFactory, ) => InjectHoc; export default MappableInjectHoc; diff --git a/packages/core/src/app/common/hoc/createInjectHoc.spec.tsx b/packages/core/src/app/common/hoc/createInjectHoc.spec.tsx index cb13e895db..566bb8e09a 100644 --- a/packages/core/src/app/common/hoc/createInjectHoc.spec.tsx +++ b/packages/core/src/app/common/hoc/createInjectHoc.spec.tsx @@ -1,5 +1,5 @@ import { mount } from 'enzyme'; -import React, { createContext, Context, FunctionComponent } from 'react'; +import React, { Context, createContext, FunctionComponent } from 'react'; import createInjectHoc from './createInjectHoc'; @@ -26,8 +26,11 @@ describe('createInjectHoc()', () => { const Inner = () =>
    ; const Outer = withFoo(Inner); - expect(mount().find(Inner).props()) - .toEqual(contextValue); + expect( + mount() + .find(Inner) + .props(), + ).toEqual(contextValue); }); it('creates HOC that injects additional props picked from context', () => { @@ -37,8 +40,11 @@ describe('createInjectHoc()', () => { const Inner = () =>
    ; const Outer = withFoo(Inner); - expect(mount().find(Inner).props()) - .toEqual({ count: contextValue.count }); + expect( + mount() + .find(Inner) + .props(), + ).toEqual({ count: contextValue.count }); }); it('creates HOC that passes outer props to inner component', () => { @@ -46,11 +52,14 @@ describe('createInjectHoc()', () => { const Inner: FunctionComponent<{ abc: string }> = () =>
    ; const Outer = withFoo(Inner); - expect(mount().find(Inner).props()) - .toEqual({ - ...contextValue, - abc: 'abc', - }); + expect( + mount() + .find(Inner) + .props(), + ).toEqual({ + ...contextValue, + abc: 'abc', + }); }); it('creates HOC with display name', () => { @@ -60,7 +69,6 @@ describe('createInjectHoc()', () => { const Inner = () =>
    ; const Outer = withFoo(Inner); - expect(mount().name()) - .toEqual('withFoo(Inner)'); + expect(mount().name()).toBe('withFoo(Inner)'); }); }); diff --git a/packages/core/src/app/common/hoc/createInjectHoc.tsx b/packages/core/src/app/common/hoc/createInjectHoc.tsx index 017f78a928..cb40efdcbe 100644 --- a/packages/core/src/app/common/hoc/createInjectHoc.tsx +++ b/packages/core/src/app/common/hoc/createInjectHoc.tsx @@ -1,6 +1,5 @@ - import { isEmpty, pickBy } from 'lodash'; -import React, { memo, useContext, ComponentType, Context, FunctionComponent } from 'react'; +import React, { ComponentType, Context, FunctionComponent, memo, useContext } from 'react'; import InjectHoc from './InjectHoc'; @@ -11,26 +10,23 @@ export interface InjectHocOptions { export default function createInjectHoc< TInjectedProps extends object | undefined, - TPickedProps extends Partial = TInjectedProps + TPickedProps extends Partial = TInjectedProps, >( ContextComponent: Context, - options?: InjectHocOptions + options?: InjectHocOptions, ): InjectHoc> { - return ( - OriginalComponent: ComponentType - ) => { - const { - displayNamePrefix = '', - pickProps = () => true, - } = options || {}; - - const InnerDecoratedComponent: FunctionComponent = memo(props => - - ); + return (OriginalComponent: ComponentType) => { + const { displayNamePrefix = '', pickProps = () => true } = options || {}; + + const InnerDecoratedComponent: FunctionComponent = memo((props) => ( + + )); const DecoratedComponent = (props: Omit>) => { const context = useContext(ContextComponent); - const injectedProps = pickBy(context, (value, key) => pickProps(value, key as keyof TInjectedProps)); + const injectedProps = pickBy(context, (value, key) => + pickProps(value, key as keyof TInjectedProps), + ); if (isEmpty(injectedProps)) { return null; @@ -38,11 +34,13 @@ export default function createInjectHoc< const mergedProps = { ...injectedProps, ...props } as unknown as TProps; - return ; + return ; }; if (displayNamePrefix) { - DecoratedComponent.displayName = `${displayNamePrefix}(${OriginalComponent.displayName || OriginalComponent.name})`; + DecoratedComponent.displayName = `${displayNamePrefix}(${ + OriginalComponent.displayName || OriginalComponent.name + })`; } return DecoratedComponent; diff --git a/packages/core/src/app/common/hoc/createMappableInjectHoc.tsx b/packages/core/src/app/common/hoc/createMappableInjectHoc.tsx index 99936e0f01..109c769fd8 100644 --- a/packages/core/src/app/common/hoc/createMappableInjectHoc.tsx +++ b/packages/core/src/app/common/hoc/createMappableInjectHoc.tsx @@ -1,47 +1,52 @@ -import React, { memo, useContext, useMemo, ComponentType, Context, FunctionComponent } from 'react'; +import React, { ComponentType, Context, FunctionComponent, memo, useContext, useMemo } from 'react'; import { Omit } from 'utility-types'; import { MatchedProps } from './InjectHoc'; import MappableInjectHoc, { MapToProps, MapToPropsFactory } from './MappableInjectHoc'; function isMapToPropsFactory( - mapToProps: ( - MapToProps | - MapToPropsFactory - ) + mapToProps: + | MapToProps + | MapToPropsFactory, ): mapToProps is MapToPropsFactory { return mapToProps.length === 0; } export default function createMappableInjectHoc( ContextComponent: Context, - options?: { displayNamePrefix?: string } + options?: { displayNamePrefix?: string }, ): MappableInjectHoc> { return ( - mapToPropsOrFactory: ( - MapToProps, TMappedProps, TOwnProps> | - MapToPropsFactory, TMappedProps, TOwnProps> - ) + mapToPropsOrFactory: + | MapToProps, TMappedProps, TOwnProps> + | MapToPropsFactory, TMappedProps, TOwnProps>, ) => { return >( - OriginalComponent: ComponentType + OriginalComponent: ComponentType, ) => { - const InnerDecoratedComponent: FunctionComponent = memo(props => - - ); + const InnerDecoratedComponent: FunctionComponent = memo((props) => ( + + )); - const DecoratedComponent: FunctionComponent> = props => { + const DecoratedComponent: FunctionComponent> = ( + props, + ) => { const context = useContext(ContextComponent); - const mapToProps = useMemo(() => ( - isMapToPropsFactory(mapToPropsOrFactory) ? - mapToPropsOrFactory() : - mapToPropsOrFactory - ), []); + const mapToProps = useMemo( + () => + isMapToPropsFactory(mapToPropsOrFactory) + ? mapToPropsOrFactory() + : mapToPropsOrFactory, + [], + ); - const mappedProps = context ? - mapToProps(context as NonNullable, props as unknown as TOwnProps) : - context; + const mappedProps = context + ? mapToProps( + context as NonNullable, + props as unknown as TOwnProps, + ) + : context; if (!mappedProps) { return null; @@ -49,11 +54,13 @@ export default function createMappableInjectHoc( const mergedProps = { ...mappedProps, ...props } as unknown as TProps; - return ; + return ; }; if (options && options.displayNamePrefix && OriginalComponent) { - DecoratedComponent.displayName = `${options.displayNamePrefix}(${OriginalComponent.displayName || OriginalComponent.name})`; + DecoratedComponent.displayName = `${options.displayNamePrefix}(${ + OriginalComponent.displayName || OriginalComponent.name + })`; } return DecoratedComponent; diff --git a/packages/core/src/app/common/request/responses.mock.ts b/packages/core/src/app/common/request/responses.mock.ts index 709ed7a17e..65f9d67583 100644 --- a/packages/core/src/app/common/request/responses.mock.ts +++ b/packages/core/src/app/common/request/responses.mock.ts @@ -1,6 +1,11 @@ import { Response } from '@bigcommerce/request-sender'; -export function getResponse(body: T, headers = {}, status = 200, statusText = 'OK'): Response { +export function getResponse( + body: T, + headers = {}, + status = 200, + statusText = 'OK', +): Response { return { body, status, diff --git a/packages/core/src/app/common/resolver/resolveComponent.spec.tsx b/packages/core/src/app/common/resolver/resolveComponent.spec.tsx index 22ea98f422..fe0c8eebb0 100644 --- a/packages/core/src/app/common/resolver/resolveComponent.spec.tsx +++ b/packages/core/src/app/common/resolver/resolveComponent.spec.tsx @@ -1,7 +1,8 @@ -import { toResolvableComponent } from '@bigcommerce/checkout/payment-integration-api'; import { render } from 'enzyme'; import React, { ComponentType } from 'react'; +import { toResolvableComponent } from '@bigcommerce/checkout/payment-integration-api'; + import resolveComponent from './resolveComponent'; describe('resolveComponent', () => { @@ -18,18 +19,18 @@ describe('resolveComponent', () => { }; const Foo = toResolvableComponent( - ({ message }: TestingProps) =>
    Foo: { message }
    , - [{ id: 'foo', gateway: null, type: 'api' }] + ({ message }: TestingProps) =>
    Foo: {message}
    , + [{ id: 'foo', gateway: null, type: 'api' }], ); const Bar = toResolvableComponent( - ({ message }: TestingProps) =>
    Bar: { message }
    , - [{ id: 'bar', gateway: null, type: 'hosted' }] + ({ message }: TestingProps) =>
    Bar: {message}
    , + [{ id: 'bar', gateway: null, type: 'hosted' }], ); const Foobar = toResolvableComponent( - ({ message }: TestingProps) =>
    Foobar: { message }
    , - [{ id: 'foo', gateway: 'bar', type: 'hosted' }] + ({ message }: TestingProps) =>
    Foobar: {message}
    , + [{ id: 'foo', gateway: 'bar', type: 'hosted' }], ); components = { Foo, Bar, Foobar }; @@ -38,42 +39,36 @@ describe('resolveComponent', () => { it('returns component if able to resolve to one by id', () => { const Foo = resolveComponent({ id: 'foo' }, components); - expect(Foo) - .toBeDefined(); - expect(Foo && render().text()) - .toEqual('Foo: Testing 123'); + expect(Foo).toBeDefined(); + expect(Foo && render().text()).toBe('Foo: Testing 123'); }); it('returns component if able to resolve to one by type', () => { const Bar = resolveComponent({ type: 'hosted' }, components); - expect(Bar) - .toBeDefined(); - expect(Bar && render().text()) - .toEqual('Bar: Testing 123'); + expect(Bar).toBeDefined(); + expect(Bar && render().text()).toBe('Bar: Testing 123'); }); it('returns component if able to resolve to one by id and gateway', () => { const Foobar = resolveComponent({ id: 'foo', gateway: 'bar' }, components); - expect(Foobar) - .toBeDefined(); - expect(Foobar && render().text()) - .toEqual('Foobar: Testing 123'); + expect(Foobar).toBeDefined(); + expect(Foobar && render().text()).toBe('Foobar: Testing 123'); }); it('returns undefined if unable to resolve to one', () => { - expect(resolveComponent({ type: 'hello' }, components)) - .toBeUndefined(); + expect(resolveComponent({ type: 'hello' }, components)).toBeUndefined(); }); it('returns default component if configured and unable to resolve by id', () => { const Default = toResolvableComponent( - ({ message }: TestingProps) =>
    Default: { message }
    , - [{ default: true }] + ({ message }: TestingProps) =>
    Default: {message}
    , + [{ default: true }], ); - expect(resolveComponent({ id: 'hello_world' }, { ...components, Default })) - .toEqual(Default); + expect(resolveComponent({ id: 'hello_world' }, { ...components, Default })).toEqual( + Default, + ); }); }); diff --git a/packages/core/src/app/common/resolver/resolveComponent.ts b/packages/core/src/app/common/resolver/resolveComponent.ts index 1ce8597a3e..582e5d3fa1 100644 --- a/packages/core/src/app/common/resolver/resolveComponent.ts +++ b/packages/core/src/app/common/resolver/resolveComponent.ts @@ -1,18 +1,16 @@ -import { isResolvableComponent } from '@bigcommerce/checkout/payment-integration-api'; import { ComponentType } from 'react'; +import { isResolvableComponent } from '@bigcommerce/checkout/payment-integration-api'; + interface ResolveResult { - component: ComponentType; - matches: number; + component: ComponentType; + matches: number; default: boolean; } -export default function resolveComponent< - TResolveId extends Record, - TProps ->( +export default function resolveComponent, TProps>( query: TResolveId, - components: Record> + components: Record>, ): ComponentType | undefined { const results: Array> = []; @@ -38,8 +36,9 @@ export default function resolveComponent< } } - const matched = results.sort((a, b) => b.matches - a.matches) - .filter(result => result.matches > 0)[0]; + const matched = results + .sort((a, b) => b.matches - a.matches) + .filter((result) => result.matches > 0)[0]; - return matched?.component ?? results.find(result => result.default)?.component; + return matched?.component ?? results.find((result) => result.default)?.component; } diff --git a/packages/core/src/app/common/types/RequireAtLeastOne.ts b/packages/core/src/app/common/types/RequireAtLeastOne.ts index 3613812976..c6deb169bb 100644 --- a/packages/core/src/app/common/types/RequireAtLeastOne.ts +++ b/packages/core/src/app/common/types/RequireAtLeastOne.ts @@ -1,7 +1,6 @@ -type RequireAtLeastOne = - Pick> - & { - [K in Keys]-?: Required> & Partial>> +type RequireAtLeastOne = Pick> & + { + [K in Keys]-?: Required> & Partial>>; }[Keys]; export default RequireAtLeastOne; diff --git a/packages/core/src/app/common/utility/isMobile.spec.ts b/packages/core/src/app/common/utility/isMobile.spec.ts index 97b65d3743..5a3c2d2b8c 100644 --- a/packages/core/src/app/common/utility/isMobile.spec.ts +++ b/packages/core/src/app/common/utility/isMobile.spec.ts @@ -3,19 +3,25 @@ import isMobile from './isMobile'; describe('isMobile()', () => { it('returns true on iPhones', () => { // @ts-ignore: setter for userAgent is defined in jest-setup.ts - window.navigator.userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1'; + window.navigator.userAgent = + 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1'; + expect(isMobile()).toBeTruthy(); }); it('returns true on Android devices', () => { // @ts-ignore: setter for userAgent is defined in jest-setup.ts - window.navigator.userAgent = 'Mozilla/5.0 (Linux; Android 7.0; SM-G930V Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36'; + window.navigator.userAgent = + 'Mozilla/5.0 (Linux; Android 7.0; SM-G930V Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36'; + expect(isMobile()).toBeTruthy(); }); it('returns false for the desktop devices', () => { // @ts-ignore: setter for userAgent is defined in jest-setup.ts - window.navigator.userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36'; + window.navigator.userAgent = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36'; + expect(isMobile()).toBeFalsy(); }); }); diff --git a/packages/core/src/app/common/utility/isMobile.ts b/packages/core/src/app/common/utility/isMobile.ts index f6b4d44273..7a3aba0dee 100644 --- a/packages/core/src/app/common/utility/isMobile.ts +++ b/packages/core/src/app/common/utility/isMobile.ts @@ -1,3 +1,3 @@ export default function isMobile(): boolean { - return /Android|iPhone|iPad|iPod/i.test(window.navigator.userAgent); + return /Android|iPhone|iPad|iPod/i.test(window.navigator.userAgent); } diff --git a/packages/core/src/app/common/utility/isRecord.ts b/packages/core/src/app/common/utility/isRecord.ts index 600468e4c8..bbf1ed2165 100644 --- a/packages/core/src/app/common/utility/isRecord.ts +++ b/packages/core/src/app/common/utility/isRecord.ts @@ -1,5 +1,5 @@ export default function isRecord( - record: unknown + record: unknown, ): record is Record { return typeof record === 'object' && record !== null; } diff --git a/packages/core/src/app/common/utility/isRecordContainingKey.ts b/packages/core/src/app/common/utility/isRecordContainingKey.ts index 43dff9e436..4ce3d4fb7f 100644 --- a/packages/core/src/app/common/utility/isRecordContainingKey.ts +++ b/packages/core/src/app/common/utility/isRecordContainingKey.ts @@ -2,7 +2,7 @@ import isRecord from './isRecord'; export default function isRecordContainingKey( record: unknown, - key: TKey + key: TKey, ): record is Record { return isRecord(record) && key in record; } diff --git a/packages/core/src/app/common/utility/joinPaths.spec.ts b/packages/core/src/app/common/utility/joinPaths.spec.ts index 9794618b0c..aeb81c48d0 100644 --- a/packages/core/src/app/common/utility/joinPaths.spec.ts +++ b/packages/core/src/app/common/utility/joinPaths.spec.ts @@ -2,26 +2,20 @@ import joinPaths from './joinPaths'; describe('joinPaths()', () => { it('joins paths with trailing slashes', () => { - expect(joinPaths('foo/', '/bar')) - .toEqual('foo/bar'); + expect(joinPaths('foo/', '/bar')).toBe('foo/bar'); - expect(joinPaths('foo/', '/bar', 'hello/', '/world')) - .toEqual('foo/bar/hello/world'); + expect(joinPaths('foo/', '/bar', 'hello/', '/world')).toBe('foo/bar/hello/world'); }); it('joins paths without trailing slashes', () => { - expect(joinPaths('foo', 'bar')) - .toEqual('foo/bar'); + expect(joinPaths('foo', 'bar')).toBe('foo/bar'); - expect(joinPaths('foo', 'bar', 'hello', 'world')) - .toEqual('foo/bar/hello/world'); + expect(joinPaths('foo', 'bar', 'hello', 'world')).toBe('foo/bar/hello/world'); }); it('retains slashes that are valid', () => { - expect(joinPaths('/foo/', '/bar/')) - .toEqual('/foo/bar/'); + expect(joinPaths('/foo/', '/bar/')).toBe('/foo/bar/'); - expect(joinPaths('/foo/', '/bar/', '/hello/', '/world/')) - .toEqual('/foo/bar/hello/world/'); + expect(joinPaths('/foo/', '/bar/', '/hello/', '/world/')).toBe('/foo/bar/hello/world/'); }); }); diff --git a/packages/core/src/app/common/utility/joinPaths.ts b/packages/core/src/app/common/utility/joinPaths.ts index 65209b2bb4..1a2581a0b2 100644 --- a/packages/core/src/app/common/utility/joinPaths.ts +++ b/packages/core/src/app/common/utility/joinPaths.ts @@ -1,12 +1,15 @@ /* eslint-disable import/export */ export default function joinPaths(first: string, second: string, ...paths: string[]): string; + export default function joinPaths(...paths: string[]): string { const first = paths.shift() || ''; const last = paths.pop() || ''; return [ first.replace(/\/$/, ''), - ...paths.map(path => path.replace(/^\/|\/$/g, '')), + ...paths.map((path) => path.replace(/^\/|\/$/g, '')), last.replace(/^\//, ''), - ].filter(value => !!value).join('/'); + ] + .filter((value) => !!value) + .join('/'); } diff --git a/packages/core/src/app/common/utility/parseAnchor.spec.ts b/packages/core/src/app/common/utility/parseAnchor.spec.ts index 3c1b0b8528..dc28a6269f 100644 --- a/packages/core/src/app/common/utility/parseAnchor.spec.ts +++ b/packages/core/src/app/common/utility/parseAnchor.spec.ts @@ -2,28 +2,30 @@ import parseAnchor from './parseAnchor'; describe('parseAnchor()', () => { it('returns empty prefix and suffix if there is just an anchor element', () => { - expect(parseAnchor('text')) - .toEqual(['', 'text', '']); + expect(parseAnchor('text')).toEqual(['', 'text', '']); - expect(parseAnchor('text')) - .toEqual(['', 'text', '']); + expect(parseAnchor('text')).toEqual(['', 'text', '']); }); it('returns prefix and suffix if anchor is surrounded by text', () => { - expect(parseAnchor('foo text bar')) - .toEqual(['foo ', 'text', ' bar']); + expect(parseAnchor('foo text bar')).toEqual(['foo ', 'text', ' bar']); - expect(parseAnchor('foo text bar')) - .toEqual(['foo ', 'text', ' bar']); + expect(parseAnchor('foo text bar')).toEqual([ + 'foo ', + 'text', + ' bar', + ]); }); it('returns first anchor if theres more than one', () => { - expect(parseAnchor('foo text s x bar')) - .toEqual(['foo ', 'text', ' s x bar']); + expect(parseAnchor('foo text s x bar')).toEqual([ + 'foo ', + 'text', + ' s x bar', + ]); }); it('returns empty array if no anchor', () => { - expect(parseAnchor('foo text bar')) - .toEqual([]); + expect(parseAnchor('foo text bar')).toEqual([]); }); }); diff --git a/packages/core/src/app/common/utility/parseAnchor.ts b/packages/core/src/app/common/utility/parseAnchor.ts index ccd1e65e7e..3af3fa3759 100644 --- a/packages/core/src/app/common/utility/parseAnchor.ts +++ b/packages/core/src/app/common/utility/parseAnchor.ts @@ -1,5 +1,6 @@ export default function parseAnchor(text: string): string[] { const div = document.createElement('div'); + div.innerHTML = text; const anchor = div.querySelector('a'); @@ -10,5 +11,5 @@ export default function parseAnchor(text: string): string[] { const anchorSiblings = div.innerHTML.split(anchor.outerHTML); - return [ anchorSiblings[0], anchor.text, anchorSiblings[1] ]; + return [anchorSiblings[0], anchor.text, anchorSiblings[1]]; } diff --git a/packages/core/src/app/common/utility/retry.spec.ts b/packages/core/src/app/common/utility/retry.spec.ts index 7a84edbfc6..86483aa040 100644 --- a/packages/core/src/app/common/utility/retry.spec.ts +++ b/packages/core/src/app/common/utility/retry.spec.ts @@ -8,10 +8,8 @@ describe('retry()', () => { try { await retry(() => call(), { count: 3, interval: 1 }); } catch (thrown) { - expect(call) - .toHaveBeenCalledTimes(3); - expect(thrown) - .toEqual(error); + expect(call).toHaveBeenCalledTimes(3); + expect(thrown).toEqual(error); } }); @@ -23,16 +21,12 @@ describe('retry()', () => { const call = jest.fn(() => { times++; - return times === 2 ? - Promise.resolve(response) : - Promise.reject(error); + return times === 2 ? Promise.resolve(response) : Promise.reject(error); }); const output = await retry(() => call(), { interval: 1 }); - expect(call) - .toHaveBeenCalledTimes(2); - expect(output) - .toEqual(response); + expect(call).toHaveBeenCalledTimes(2); + expect(output).toEqual(response); }); }); diff --git a/packages/core/src/app/common/utility/retry.ts b/packages/core/src/app/common/utility/retry.ts index 00555a9023..46ce1a96d4 100644 --- a/packages/core/src/app/common/utility/retry.ts +++ b/packages/core/src/app/common/utility/retry.ts @@ -8,10 +8,7 @@ export interface RetryOptions { interval?: number; } -export default async function retry( - fn: () => Promise, - options?: RetryOptions -): Promise { +export default async function retry(fn: () => Promise, options?: RetryOptions): Promise { const { count, interval } = { ...DEFAULT_OPTIONS, ...options }; try { @@ -21,7 +18,7 @@ export default async function retry( throw error; } - await new Promise(resolve => setTimeout(resolve, interval)); + await new Promise((resolve) => setTimeout(resolve, interval)); return retry(fn, { interval, count: count - 1 }); } diff --git a/packages/core/src/app/config/config.mock.ts b/packages/core/src/app/config/config.mock.ts index c338f1aee5..3b10fad9a7 100644 --- a/packages/core/src/app/config/config.mock.ts +++ b/packages/core/src/app/config/config.mock.ts @@ -33,13 +33,10 @@ export function getStoreConfig(): StoreConfig { orderTermsAndConditionsType: '', privacyPolicyUrl: '', providerWithCustomCheckout: null, - shippingQuoteFailedMessage: 'Unfortunately one or more items in your cart can\'t be shipped to your \ - location.Please choose a different delivery address.', - realtimeShippingProviders: [ - 'Fedex', - 'UPS', - 'USPS', - ], + shippingQuoteFailedMessage: + "Unfortunately one or more items in your cart can't be shipped to your \ + location.Please choose a different delivery address.", + realtimeShippingProviders: ['Fedex', 'UPS', 'USPS'], requiresMarketingConsent: false, features: {}, remoteCheckoutProviders: [], diff --git a/packages/core/src/app/coupon/AppliedCoupon.spec.tsx b/packages/core/src/app/coupon/AppliedCoupon.spec.tsx index 2609e0dd30..796f226038 100644 --- a/packages/core/src/app/coupon/AppliedCoupon.spec.tsx +++ b/packages/core/src/app/coupon/AppliedCoupon.spec.tsx @@ -1,14 +1,14 @@ import { shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; -import { getCoupon } from './coupons.mock'; import AppliedCoupon from './AppliedCoupon'; +import { getCoupon } from './coupons.mock'; describe('AppliedCoupon', () => { let component: ShallowWrapper; beforeEach(() => { - component = shallow(); + component = shallow(); }); it('renders markup that matches snapshot', () => { diff --git a/packages/core/src/app/coupon/AppliedCoupon.tsx b/packages/core/src/app/coupon/AppliedCoupon.tsx index c54b5fc764..173c4fbff9 100644 --- a/packages/core/src/app/coupon/AppliedCoupon.tsx +++ b/packages/core/src/app/coupon/AppliedCoupon.tsx @@ -1,5 +1,5 @@ import { Coupon } from '@bigcommerce/checkout-sdk'; -import React, { memo, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo } from 'react'; import { TranslatedString } from '../locale'; @@ -8,25 +8,16 @@ export interface AppliedCouponProps { } const AppliedCoupon: FunctionComponent = ({ coupon }) => ( -
    +
    - - { coupon.displayName } - - - { ' ' } - + + {coupon.displayName} + {' '} - { coupon.code } + {coupon.code}
    ); diff --git a/packages/core/src/app/currency/ShopperCurrency.spec.tsx b/packages/core/src/app/currency/ShopperCurrency.spec.tsx index e3ae56bdcd..7fb5891327 100644 --- a/packages/core/src/app/currency/ShopperCurrency.spec.tsx +++ b/packages/core/src/app/currency/ShopperCurrency.spec.tsx @@ -16,9 +16,9 @@ describe('ShopperCurrency Component', () => { const tree = testRenderer .create( - - - + + + , ) .toJSON(); diff --git a/packages/core/src/app/currency/ShopperCurrency.tsx b/packages/core/src/app/currency/ShopperCurrency.tsx index 4c7ee9efe7..8901a841a4 100644 --- a/packages/core/src/app/currency/ShopperCurrency.tsx +++ b/packages/core/src/app/currency/ShopperCurrency.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, FunctionComponent } from 'react'; +import React, { FunctionComponent } from 'react'; import { withCurrency, WithCurrencyProps } from '../locale'; @@ -9,10 +9,6 @@ export interface ShopperCurrencyProps { const ShopperCurrency: FunctionComponent = ({ amount, currency, -}) => ( - - { currency.toCustomerCurrency(amount) } - -); +}) => <>{currency.toCustomerCurrency(amount)}; export default withCurrency(ShopperCurrency); diff --git a/packages/core/src/app/currency/StoreCurrency.spec.tsx b/packages/core/src/app/currency/StoreCurrency.spec.tsx index 649f75b0d2..71319a2d87 100644 --- a/packages/core/src/app/currency/StoreCurrency.spec.tsx +++ b/packages/core/src/app/currency/StoreCurrency.spec.tsx @@ -14,9 +14,9 @@ describe('ShopperCurrency Component', () => { const tree = testRenderer .create( - - - + + + , ) .toJSON(); diff --git a/packages/core/src/app/currency/StoreCurrency.tsx b/packages/core/src/app/currency/StoreCurrency.tsx index 080418f420..2dca9e4018 100644 --- a/packages/core/src/app/currency/StoreCurrency.tsx +++ b/packages/core/src/app/currency/StoreCurrency.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, FunctionComponent } from 'react'; +import React, { FunctionComponent } from 'react'; import { withCurrency, WithCurrencyProps } from '../locale'; @@ -9,10 +9,6 @@ export interface StoreCurrencyProps { const StoreCurrency: FunctionComponent = ({ amount, currency, -}) => ( - - { currency.toStoreCurrency(amount) } - -); +}) => <>{currency.toStoreCurrency(amount)}; export default withCurrency(StoreCurrency); diff --git a/packages/core/src/app/customer/CheckoutButton.spec.tsx b/packages/core/src/app/customer/CheckoutButton.spec.tsx index 801ee76918..50ca5b0309 100644 --- a/packages/core/src/app/customer/CheckoutButton.spec.tsx +++ b/packages/core/src/app/customer/CheckoutButton.spec.tsx @@ -12,21 +12,20 @@ describe('CheckoutButton', () => { mount( + onError={onError} + />, ); - expect(initialize) - .toHaveBeenCalledWith({ - methodId: 'foobar', - foobar: { - container: 'foobarContainer', - onError, - }, - }); + expect(initialize).toHaveBeenCalledWith({ + methodId: 'foobar', + foobar: { + container: 'foobarContainer', + onError, + }, + }); }); it('deinitializes button when component unmounts', () => { @@ -36,16 +35,15 @@ describe('CheckoutButton', () => { const component = mount( + onError={onError} + />, ); component.unmount(); - expect(deinitialize) - .toHaveBeenCalled(); + expect(deinitialize).toHaveBeenCalled(); }); }); diff --git a/packages/core/src/app/customer/CheckoutButton.tsx b/packages/core/src/app/customer/CheckoutButton.tsx index c07777ba03..5a967cb85d 100644 --- a/packages/core/src/app/customer/CheckoutButton.tsx +++ b/packages/core/src/app/customer/CheckoutButton.tsx @@ -11,12 +11,7 @@ export interface CheckoutButtonProps { export default class CheckoutButton extends PureComponent { componentDidMount() { - const { - containerId, - initialize, - methodId, - onError, - } = this.props; + const { containerId, initialize, methodId, onError } = this.props; initialize({ methodId, @@ -28,10 +23,7 @@ export default class CheckoutButton extends PureComponent { } componentWillUnmount() { - const { - deinitialize, - methodId, - } = this.props; + const { deinitialize, methodId } = this.props; deinitialize({ methodId }); } @@ -39,8 +31,6 @@ export default class CheckoutButton extends PureComponent { render() { const { containerId } = this.props; - return ( -
    - ); + return
    ; } } diff --git a/packages/core/src/app/customer/CheckoutButtonList.spec.tsx b/packages/core/src/app/customer/CheckoutButtonList.spec.tsx index b131899064..582222eaee 100644 --- a/packages/core/src/app/customer/CheckoutButtonList.spec.tsx +++ b/packages/core/src/app/customer/CheckoutButtonList.spec.tsx @@ -17,122 +17,109 @@ describe('CheckoutButtonList', () => { it('matches snapshot', () => { const component = render( - + - + , ); - expect(component) - .toMatchSnapshot(); + expect(component).toMatchSnapshot(); }); it('filters out unsupported methods', () => { const component = mount( - + - + , ); - expect(component.find(CheckoutButton)) - .toHaveLength(2); + expect(component.find(CheckoutButton)).toHaveLength(2); }); it('does not crash when no methods are passed', () => { const component = mount( - - - + + + , ); - expect(component.html()) - .toBeFalsy(); + expect(component.html()).toBeFalsy(); }); it('does not render if there are no supported methods', () => { const component = mount( - - - + + + , ); - expect(component.html()) - .toBeFalsy(); + expect(component.html()).toBeFalsy(); }); it('does not render the translated string when initializing', () => { const component = mount( - + - + , ); - expect(component.find(TranslatedString)) - .toHaveLength(0); + expect(component.find(TranslatedString)).toHaveLength(0); }); it('passes data to every checkout button', () => { const deinitialize = jest.fn(); const initialize = jest.fn(); const component = mount( - + - + , ); - expect(component.find(CheckoutButton).at(0).props()) - .toEqual({ - containerId: 'amazonCheckoutButton', - methodId: 'amazon', - deinitialize, - initialize, - }); + expect(component.find(CheckoutButton).at(0).props()).toEqual({ + containerId: 'amazonCheckoutButton', + methodId: 'amazon', + deinitialize, + initialize, + }); }); it('notifies parent if methods are incompatible with Embedded Checkout', () => { const methodIds = ['amazon', 'braintreevisacheckout']; const onError = jest.fn(); - const checkEmbeddedSupport = jest.fn(() => { throw new Error(); }); + const checkEmbeddedSupport = jest.fn(() => { + throw new Error(); + }); render( - + - + , ); - expect(checkEmbeddedSupport) - .toHaveBeenCalledWith(methodIds); + expect(checkEmbeddedSupport).toHaveBeenCalledWith(methodIds); - expect(onError) - .toHaveBeenCalledWith(expect.any(Error)); + expect(onError).toHaveBeenCalledWith(expect.any(Error)); }); }); diff --git a/packages/core/src/app/customer/CheckoutButtonList.tsx b/packages/core/src/app/customer/CheckoutButtonList.tsx index 9697e8d0ee..6190b27fe5 100644 --- a/packages/core/src/app/customer/CheckoutButtonList.tsx +++ b/packages/core/src/app/customer/CheckoutButtonList.tsx @@ -1,11 +1,11 @@ import { CustomerInitializeOptions, CustomerRequestOptions } from '@bigcommerce/checkout-sdk'; -import React, { memo, Fragment, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo } from 'react'; import { isApplePayWindow } from '../common/utility'; import { TranslatedString } from '../locale'; -import { ApplePayButton, AmazonPayV2Button } from './customWalletButton'; import CheckoutButton from './CheckoutButton'; +import { AmazonPayV2Button, ApplePayButton } from './customWalletButton'; const APPLE_PAY = 'applepay'; @@ -44,7 +44,7 @@ const CheckoutButtonList: FunctionComponent = ({ methodIds, ...rest }) => { - const supportedMethodIds = (methodIds ?? []).filter(methodId => { + const supportedMethodIds = (methodIds ?? []).filter((methodId) => { if (methodId === APPLE_PAY && !isApplePayWindow(window)) { return false; } @@ -71,41 +71,51 @@ const CheckoutButtonList: FunctionComponent = ({ } return ( - - { !isInitializing &&

    } + <> + {!isInitializing && ( +

    + +

    + )}
    - { supportedMethodIds.map(methodId => { + {supportedMethodIds.map((methodId) => { if (methodId === 'applepay') { - return ; + return ( + + ); } if (methodId === 'amazonpay') { - return ; + return ( + + ); } - return - }) } + return ( + + ); + })}
    -
    + ); }; diff --git a/packages/core/src/app/customer/CheckoutButtonListV2.spec.tsx b/packages/core/src/app/customer/CheckoutButtonListV2.spec.tsx index 37122b92e6..b23a9e9102 100644 --- a/packages/core/src/app/customer/CheckoutButtonListV2.spec.tsx +++ b/packages/core/src/app/customer/CheckoutButtonListV2.spec.tsx @@ -1,22 +1,26 @@ -import { createCheckoutService, CheckoutService, CheckoutSelectors } from '@bigcommerce/checkout-sdk'; -import { CheckoutButtonProps, CheckoutButtonResolveId } from '@bigcommerce/checkout/payment-integration-api'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, +} from '@bigcommerce/checkout-sdk'; import { mount } from 'enzyme'; import { merge, noop } from 'lodash'; import React, { ComponentType } from 'react'; +import { + CheckoutButtonProps, + CheckoutButtonResolveId, +} from '@bigcommerce/checkout/payment-integration-api'; + import { CheckoutProvider } from '../checkout'; -import { LocaleProvider } from '../locale'; import { getStoreConfig } from '../config/config.mock'; +import { LocaleProvider } from '../locale'; import CheckoutButtonList, { CheckoutButtonListProps } from './CheckoutButtonListV2'; -const FooButton: ComponentType = () => ( - -); +const FooButton: ComponentType = () => ; -const DefaultButton: ComponentType = () => ( - -); +const DefaultButton: ComponentType = () => ; jest.mock('./resolveCheckoutButton', () => { return ({ id }: CheckoutButtonResolveId) => { @@ -42,53 +46,44 @@ describe('CheckoutButtonListV2', () => { checkoutService = createCheckoutService(); checkoutState = checkoutService.getState(); - jest.spyOn(checkoutService, 'subscribe') - .mockImplementation(subscriber => { - subscriber(checkoutState); + jest.spyOn(checkoutService, 'subscribe').mockImplementation((subscriber) => { + subscriber(checkoutState); - return noop; - }); + return noop; + }); - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(merge( - getStoreConfig(), - { - checkoutSettings: { - remoteCheckoutProviders: ['foo', 'bar'], - }, - } - )); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue( + merge(getStoreConfig(), { + checkoutSettings: { + remoteCheckoutProviders: ['foo', 'bar'], + }, + }), + ); CheckoutButtonListTest = () => ( - - - + + + ); }); it('renders list of checkout buttons', () => { - jest.spyOn(checkoutState.statuses, 'isInitializingCustomer') - .mockReturnValue(false); + jest.spyOn(checkoutState.statuses, 'isInitializingCustomer').mockReturnValue(false); - const component = mount(); + const component = mount(); - expect(component.find(DefaultButton).length) - .toEqual(1); - expect(component.find(FooButton).length) - .toEqual(1); - expect(component.html()) - .toContain('

    Or continue with

    '); + expect(component.find(DefaultButton)).toHaveLength(1); + expect(component.find(FooButton)).toHaveLength(1); + expect(component.html()).toContain('

    Or continue with

    '); }); it('does not render "or continue with" while initializing', () => { - jest.spyOn(checkoutState.statuses, 'isInitializingCustomer') - .mockReturnValue(true); + jest.spyOn(checkoutState.statuses, 'isInitializingCustomer').mockReturnValue(true); - const component = mount(); + const component = mount(); - expect(component.html()) - .not.toContain('

    Or continue with

    '); + expect(component.html()).not.toContain('

    Or continue with

    '); }); }); diff --git a/packages/core/src/app/customer/CheckoutButtonListV2.tsx b/packages/core/src/app/customer/CheckoutButtonListV2.tsx index 553abe7c10..7f00405f25 100644 --- a/packages/core/src/app/customer/CheckoutButtonListV2.tsx +++ b/packages/core/src/app/customer/CheckoutButtonListV2.tsx @@ -1,7 +1,8 @@ -import React, { Fragment, FunctionComponent } from 'react'; +import React, { FunctionComponent } from 'react'; import { withCheckout, WithCheckoutProps } from '../checkout'; import { TranslatedString, withLanguage, WithLanguageProps } from '../locale'; + import resolveCheckoutButton from './resolveCheckoutButton'; export interface CheckoutButtonListProps { @@ -9,49 +10,46 @@ export interface CheckoutButtonListProps { } const CheckoutButtonList: FunctionComponent< - CheckoutButtonListProps & - WithCheckoutProps & - WithLanguageProps -> = ({ - checkoutService, - checkoutState, - language, - onUnhandledError -}) => { - const { + CheckoutButtonListProps & WithCheckoutProps & WithLanguageProps +> = ({ checkoutService, checkoutState, language, onUnhandledError }) => { + const { statuses: { isInitializingCustomer }, data: { getConfig }, } = checkoutState; - const methodIds = getConfig()?.checkoutSettings?.remoteCheckoutProviders ?? []; + const methodIds = getConfig()?.checkoutSettings.remoteCheckoutProviders ?? []; return ( - - { !isInitializingCustomer() &&

    } + <> + {!isInitializingCustomer() && ( +

    + +

    + )}
    - { methodIds.map(methodId => { + {methodIds.map((methodId) => { const ResolvedCheckoutButton = resolveCheckoutButton({ id: methodId }); if (!ResolvedCheckoutButton) { return null; } - return - }) } + return ( + + ); + })}
    -
    + ); }; -export default withCheckout(props => props)( - withLanguage(CheckoutButtonList) -); +export default withCheckout((props) => props)(withLanguage(CheckoutButtonList)); diff --git a/packages/core/src/app/customer/CreateAccountForm.spec.tsx b/packages/core/src/app/customer/CreateAccountForm.spec.tsx index e45202f9dc..e2caa7f229 100644 --- a/packages/core/src/app/customer/CreateAccountForm.spec.tsx +++ b/packages/core/src/app/customer/CreateAccountForm.spec.tsx @@ -9,8 +9,8 @@ import { createLocaleContext, LocaleContext, LocaleContextType, TranslatedString import { Alert } from '../ui/alert'; import { DynamicFormField } from '../ui/form'; -import { getCustomerAccountFormFields } from './formField.mock'; import CreateAccountForm from './CreateAccountForm'; +import { getCustomerAccountFormFields } from './formField.mock'; describe('CreateAccountForm Component', () => { let localeContext: LocaleContextType; @@ -24,25 +24,19 @@ describe('CreateAccountForm Component', () => { it('renders all fields based on formFields', () => { component = mount( - - - + + + - + , ); - expect(component.find(DynamicFormField).length).toEqual(formFields.length); + expect(component.find(DynamicFormField)).toHaveLength(formFields.length); expect(component.find(DynamicFormField).at(0).prop('field')).toEqual( expect.objectContaining({ id: 'field_4', - }) + }), ); }); @@ -55,14 +49,14 @@ describe('CreateAccountForm Component', () => { } as RequestError; component = mount( - + - + , ); expect(component.find(Alert).find(TranslatedString).props()).toEqual({ @@ -71,48 +65,50 @@ describe('CreateAccountForm Component', () => { }); }); - it.each([['Password needs to contain a letter', '1234567'], ['Password needs to contain a number', 'abcdefg'], ['Password is too short', '1a']]) ('renders correct error when %s', async (expected, passwordCase) => { + it.each([ + ['Password needs to contain a letter', '1234567'], + ['Password needs to contain a number', 'abcdefg'], + ['Password is too short', '1a'], + ])('renders correct error when %s', async (expected, passwordCase) => { const onSubmit = jest.fn(); component = mount( - + - + , ); - component.find('input[name="password"]') + component + .find('input[name="password"]') .simulate('change', { target: { value: passwordCase, name: 'password' } }); - component.find('form') - .simulate('submit'); + component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(component.find('#password-field-error-message').text()) - .toEqual(expected); + expect(component.find('#password-field-error-message').text()).toEqual(expected); }); it('calls onCancel', () => { const onCancel = jest.fn(); component = mount( - + - + , ); - component.find('[data-test="customer-cancel-button"]') - .simulate('click'); + component.find('[data-test="customer-cancel-button"]').simulate('click'); expect(onCancel).toHaveBeenCalled(); }); @@ -121,91 +117,99 @@ describe('CreateAccountForm Component', () => { const onSubmit = jest.fn(); component = mount( - + - + , ); - component.find('input[name="email"]') + component + .find('input[name="email"]') .simulate('change', { target: { value: 'test@bigcommerce.com', name: 'email' } }); - component.find('form') - .simulate('submit'); + component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); expect(onSubmit).not.toHaveBeenCalled(); - component.find('input[name="password"]') + component + .find('input[name="password"]') .simulate('change', { target: { value: 'Password1235+', name: 'password' } }); - component.find('input[name="firstName"]') + component + .find('input[name="firstName"]') .simulate('change', { target: { value: 'foo', name: 'firstName' } }); - component.find('input[name="lastName"]') + component + .find('input[name="lastName"]') .simulate('change', { target: { value: 'bar', name: 'lastName' } }); - component.find('form') - .simulate('submit'); + component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({ - acceptsMarketingEmails: ['0'], - email: 'test@bigcommerce.com', - firstName: 'foo', - lastName: 'bar', - password: 'Password1235+', - })); + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + acceptsMarketingEmails: ['0'], + email: 'test@bigcommerce.com', + firstName: 'foo', + lastName: 'bar', + password: 'Password1235+', + }), + ); }); it('calls onSubmit when form is valid and requires consent', async () => { const onSubmit = jest.fn(); component = mount( - + - + , ); - component.find('input[name="email"]') + component + .find('input[name="email"]') .simulate('change', { target: { value: 'test@bigcommerce.com', name: 'email' } }); - component.find('form') - .simulate('submit'); + component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); expect(onSubmit).not.toHaveBeenCalled(); - component.find('input[name="password"]') + component + .find('input[name="password"]') .simulate('change', { target: { value: 'Password1235+', name: 'password' } }); - component.find('input[name="firstName"]') + component + .find('input[name="firstName"]') .simulate('change', { target: { value: 'foo', name: 'firstName' } }); - component.find('input[name="lastName"]') + component + .find('input[name="lastName"]') .simulate('change', { target: { value: 'bar', name: 'lastName' } }); - component.find('form') - .simulate('submit'); + component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({ - email: 'test@bigcommerce.com', - firstName: 'foo', - lastName: 'bar', - password: 'Password1235+', - acceptsMarketingEmails: [], - })); + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'test@bigcommerce.com', + firstName: 'foo', + lastName: 'bar', + password: 'Password1235+', + acceptsMarketingEmails: [], + }), + ); }); }); diff --git a/packages/core/src/app/customer/CreateAccountForm.tsx b/packages/core/src/app/customer/CreateAccountForm.tsx index aff05ed938..29511864ca 100644 --- a/packages/core/src/app/customer/CreateAccountForm.tsx +++ b/packages/core/src/app/customer/CreateAccountForm.tsx @@ -1,16 +1,18 @@ import { FormField } from '@bigcommerce/checkout-sdk'; -import { withFormik, FormikProps } from 'formik'; +import { FormikProps, withFormik } from 'formik'; import { noop } from 'lodash'; -import React, { useMemo, FunctionComponent } from 'react'; +import React, { FunctionComponent, useMemo } from 'react'; import { preventDefault } from '../common/dom'; import { isRequestError } from '../common/error'; -import { withLanguage, TranslatedString, WithLanguageProps } from '../locale'; +import { TranslatedString, withLanguage, WithLanguageProps } from '../locale'; import { Alert, AlertType } from '../ui/alert'; import { Button, ButtonVariant } from '../ui/button'; import { DynamicFormField, Fieldset, Form } from '../ui/form'; -import getCreateCustomerValidationSchema, { CreateAccountFormValues } from './getCreateCustomerValidationSchema'; +import getCreateCustomerValidationSchema, { + CreateAccountFormValues, +} from './getCreateCustomerValidationSchema'; import getPasswordRequirements from './getPasswordRequirements'; import './CreateAccountForm.scss'; @@ -23,12 +25,9 @@ export interface CreateAccountFormProps { onSubmit?(values: CreateAccountFormValues): void; } -const CreateAccountForm: FunctionComponent> = ({ - formFields, - createAccountError, - isCreatingAccount, - onCancel, -}) => { +const CreateAccountForm: FunctionComponent< + CreateAccountFormProps & WithLanguageProps & FormikProps +> = ({ formFields, createAccountError, isCreatingAccount, onCancel }) => { const createAccountErrorMessage = useMemo(() => { if (!createAccountError) { return; @@ -38,10 +37,12 @@ const CreateAccountForm: FunctionComponent 1) { - return ; + return ( + + ); } return ; @@ -50,38 +51,36 @@ const CreateAccountForm: FunctionComponent + return (
    - { createAccountErrorMessage && - { createAccountErrorMessage } - } + {createAccountErrorMessage && ( + {createAccountErrorMessage} + )}
    - { formFields.map(field => ( + {formFields.map((field) => ( - )) } + ))}
    -
    +
    @@ -91,43 +90,47 @@ const CreateAccountForm: FunctionComponent
    - ); + ); }; -export default withLanguage(withFormik({ - handleSubmit: (values, { props: { onSubmit = noop } }) => { - onSubmit(values); - }, - mapPropsToValues: ({requiresMarketingConsent}) => ({ - firstName: '', - lastName: '', - email: '', - password: '', - customFields: {}, - acceptsMarketingEmails: requiresMarketingConsent ? [] : ['0'], - }), - validationSchema: ({ - language, - formFields, - }: CreateAccountFormProps & WithLanguageProps) => { - const passwordRequirements = formFields.find(({ requirements }) => requirements)?.requirements; - - if (!passwordRequirements) { - throw new Error('Password requirements missing'); - } - - const schema = getCreateCustomerValidationSchema({ +export default withLanguage( + withFormik({ + handleSubmit: (values, { props: { onSubmit = noop } }) => { + onSubmit(values); + }, + mapPropsToValues: ({ requiresMarketingConsent }) => ({ + firstName: '', + lastName: '', + email: '', + password: '', + customFields: {}, + acceptsMarketingEmails: requiresMarketingConsent ? [] : ['0'], + }), + validationSchema: ({ language, formFields, - passwordRequirements: getPasswordRequirements(passwordRequirements), - }); + }: CreateAccountFormProps & WithLanguageProps) => { + const passwordRequirements = formFields.find( + ({ requirements }) => requirements, + )?.requirements; + + if (!passwordRequirements) { + throw new Error('Password requirements missing'); + } + + const schema = getCreateCustomerValidationSchema({ + language, + formFields, + passwordRequirements: getPasswordRequirements(passwordRequirements), + }); - return schema; - }, -})(CreateAccountForm)); + return schema; + }, + })(CreateAccountForm), +); diff --git a/packages/core/src/app/customer/Customer.spec.tsx b/packages/core/src/app/customer/Customer.spec.tsx index 720bd005a4..9a70a6e819 100644 --- a/packages/core/src/app/customer/Customer.spec.tsx +++ b/packages/core/src/app/customer/Customer.spec.tsx @@ -1,4 +1,12 @@ -import { createCheckoutService, BillingAddress, Checkout, CheckoutService, Customer as CustomerData, StoreConfig, RequestError } from '@bigcommerce/checkout-sdk'; +import { + BillingAddress, + Checkout, + CheckoutService, + createCheckoutService, + Customer as CustomerData, + RequestError, + StoreConfig, +} from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import React, { FunctionComponent } from 'react'; @@ -8,9 +16,9 @@ import { getCheckout } from '../checkout/checkouts.mock'; import { getStoreConfig } from '../config/config.mock'; import { createLocaleContext, LocaleContext, LocaleContextType } from '../locale'; -import { getCustomer, getGuestCustomer } from './customers.mock'; import CreateAccountForm from './CreateAccountForm'; import Customer, { CustomerProps, WithCheckoutCustomerProps } from './Customer'; +import { getCustomer, getGuestCustomer } from './customers.mock'; import CustomerViewType from './CustomerViewType'; import EmailLoginForm from './EmailLoginForm'; import GuestForm, { GuestFormProps } from './GuestForm'; @@ -33,33 +41,32 @@ describe('Customer', () => { checkoutService = createCheckoutService(); - jest.spyOn(checkoutService.getState().data, 'getBillingAddress') - .mockReturnValue(billingAddress); + jest.spyOn(checkoutService.getState().data, 'getBillingAddress').mockReturnValue( + billingAddress, + ); - jest.spyOn(checkoutService.getState().data, 'getCheckout') - .mockReturnValue(checkout); + jest.spyOn(checkoutService.getState().data, 'getCheckout').mockReturnValue(checkout); - jest.spyOn(checkoutService.getState().data, 'getCustomer') - .mockReturnValue(customer); + jest.spyOn(checkoutService.getState().data, 'getCustomer').mockReturnValue(customer); - jest.spyOn(checkoutService.getState().data, 'getConfig') - .mockReturnValue(config); + jest.spyOn(checkoutService.getState().data, 'getConfig').mockReturnValue(config); - jest.spyOn(checkoutService, 'loadPaymentMethods') - .mockResolvedValue(checkoutService.getState()); + jest.spyOn(checkoutService, 'loadPaymentMethods').mockResolvedValue( + checkoutService.getState(), + ); - jest.spyOn(checkoutService, 'initializeCustomer') - .mockResolvedValue(checkoutService.getState()); + jest.spyOn(checkoutService, 'initializeCustomer').mockResolvedValue( + checkoutService.getState(), + ); - jest.spyOn(checkoutService.getState().data, 'getPaymentMethods') - .mockReturnValue([]); + jest.spyOn(checkoutService.getState().data, 'getPaymentMethods').mockReturnValue([]); localeContext = createLocaleContext(getStoreConfig()); - CustomerTest = props => ( - - - + CustomerTest = (props) => ( + + + ); @@ -67,291 +74,255 @@ describe('Customer', () => { describe('when view type is "guest"', () => { it('matches snapshot', async () => { - const component = mount( - - ); + const component = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(component.render()) - .toMatchSnapshot(); + expect(component.render()).toMatchSnapshot(); }); it('renders guest form by default', async () => { - const component = mount( - - ); + const component = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(component.find(GuestForm).exists()) - .toEqual(true); + expect(component.find(GuestForm).exists()).toBe(true); }); it('renders guest form if billing address is undefined', async () => { - jest.spyOn(checkoutService.getState().data, 'getBillingAddress') - .mockReturnValue(undefined); - - const component = mount( - + jest.spyOn(checkoutService.getState().data, 'getBillingAddress').mockReturnValue( + undefined, ); - await new Promise(resolve => process.nextTick(resolve)); + const component = mount(); + + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(component.find(GuestForm).exists()) - .toEqual(true); + expect(component.find(GuestForm).exists()).toBe(true); }); it('renders guest form if customer is undefined', async () => { - jest.spyOn(checkoutService.getState().data, 'getCustomer') - .mockReturnValue(undefined); + jest.spyOn(checkoutService.getState().data, 'getCustomer').mockReturnValue(undefined); - const component = mount( - - ); + const component = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(component.find(GuestForm).exists()) - .toEqual(true); + expect(component.find(GuestForm).exists()).toBe(true); }); it('renders create account form if type is create account', async () => { - jest.spyOn(checkoutService.getState().data, 'getCustomer') - .mockReturnValue(undefined); - - jest.spyOn(checkoutService.getState().data, 'getConfig') - .mockReturnValue({ - ...getStoreConfig(), - checkoutSettings: { - ...getStoreConfig().checkoutSettings, - features: { - 'CHECKOUT-4941.account_creation_in_checkout': true, - }, + jest.spyOn(checkoutService.getState().data, 'getCustomer').mockReturnValue(undefined); + + jest.spyOn(checkoutService.getState().data, 'getConfig').mockReturnValue({ + ...getStoreConfig(), + checkoutSettings: { + ...getStoreConfig().checkoutSettings, + features: { + 'CHECKOUT-4941.account_creation_in_checkout': true, }, - }); + }, + }); - const component = mount( - - ); + const component = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(component.find(CreateAccountForm).exists()) - .toEqual(true); + expect(component.find(CreateAccountForm).exists()).toBe(true); }); it('passes data to guest form', async () => { - const component = mount( - - ); + const component = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(component.find(GuestForm).props()) - .toMatchObject({ - canSubscribe: config.shopperConfig.showNewsletterSignup, - defaultShouldSubscribe: config.shopperConfig.defaultNewsletterSignup, - email: billingAddress.email, - }); + expect(component.find(GuestForm).props()).toMatchObject({ + canSubscribe: config.shopperConfig.showNewsletterSignup, + defaultShouldSubscribe: config.shopperConfig.defaultNewsletterSignup, + email: billingAddress.email, + }); }); it('continues checkout as guest and does not send consent if not required', async () => { - jest.spyOn(checkoutService, 'continueAsGuest') - .mockReturnValue(Promise.resolve(checkoutService.getState())); - - const component = mount( - + jest.spyOn(checkoutService, 'continueAsGuest').mockReturnValue( + Promise.resolve(checkoutService.getState()), ); - await new Promise(resolve => process.nextTick(resolve)); + const component = mount(); + + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - (component.find(GuestForm) as ReactWrapper) - .prop('onContinueAsGuest')({ - email: ' test@bigcommerce.com ', - shouldSubscribe: true, - }); + (component.find(GuestForm) as ReactWrapper).prop('onContinueAsGuest')({ + email: ' test@bigcommerce.com ', + shouldSubscribe: true, + }); - expect(checkoutService.continueAsGuest) - .toHaveBeenCalledWith({ - email: 'test@bigcommerce.com', - acceptsMarketingNewsletter: true, - acceptsAbandonedCartEmails: true, - }); + expect(checkoutService.continueAsGuest).toHaveBeenCalledWith({ + email: 'test@bigcommerce.com', + acceptsMarketingNewsletter: true, + acceptsAbandonedCartEmails: true, + }); }); it('only subscribes to newsletter if allowed by customer', async () => { - jest.spyOn(checkoutService, 'continueAsGuest') - .mockReturnValue(Promise.resolve(checkoutService.getState())); + jest.spyOn(checkoutService, 'continueAsGuest').mockReturnValue( + Promise.resolve(checkoutService.getState()), + ); const subscribeToNewsletter = jest.fn(); - const component = mount( - - ); + const component = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - (component.find(GuestForm) as ReactWrapper) - .prop('onContinueAsGuest')({ - email: 'test@bigcommerce.com', - shouldSubscribe: false, - }); + (component.find(GuestForm) as ReactWrapper).prop('onContinueAsGuest')({ + email: 'test@bigcommerce.com', + shouldSubscribe: false, + }); - expect(checkoutService.continueAsGuest) - .toHaveBeenCalledWith({ - email: 'test@bigcommerce.com', - acceptsMarketingNewsletter: undefined, - acceptsAbandonedCartEmails: undefined, - }); + expect(checkoutService.continueAsGuest).toHaveBeenCalledWith({ + email: 'test@bigcommerce.com', + acceptsMarketingNewsletter: undefined, + acceptsAbandonedCartEmails: undefined, + }); - expect(subscribeToNewsletter) - .not.toHaveBeenCalled(); + expect(subscribeToNewsletter).not.toHaveBeenCalled(); }); it('changes to login view when "show login" event is received', async () => { const handleChangeViewType = jest.fn(); const component = mount( + onChangeViewType={handleChangeViewType} + viewType={CustomerViewType.Guest} + />, ); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - (component.find(GuestForm) as ReactWrapper) - .prop('onShowLogin')(); + (component.find(GuestForm) as ReactWrapper).prop('onShowLogin')(); - expect(handleChangeViewType) - .toHaveBeenCalledWith(CustomerViewType.Login); + expect(handleChangeViewType).toHaveBeenCalledWith(CustomerViewType.Login); }); it('triggers completion callback if customer successfully continued as guest', async () => { - jest.spyOn(checkoutService, 'continueAsGuest') - .mockReturnValue(Promise.resolve(checkoutService.getState())); + jest.spyOn(checkoutService, 'continueAsGuest').mockReturnValue( + Promise.resolve(checkoutService.getState()), + ); const handleContinueAsGuest = jest.fn(); const component = mount( + onContinueAsGuest={handleContinueAsGuest} + viewType={CustomerViewType.Guest} + />, ); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - (component.find(GuestForm) as ReactWrapper) - .prop('onContinueAsGuest')({ - email: 'test@bigcommerce.com', - shouldSubscribe: false, - }); + (component.find(GuestForm) as ReactWrapper).prop('onContinueAsGuest')({ + email: 'test@bigcommerce.com', + shouldSubscribe: false, + }); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(handleContinueAsGuest) - .toHaveBeenCalled(); + expect(handleContinueAsGuest).toHaveBeenCalled(); }); it('triggers completion callback if continueAsGuest fails with update_subscriptions', async () => { const error = { type: 'update_subscriptions' }; - jest.spyOn(checkoutService, 'continueAsGuest') - .mockRejectedValue(error); + jest.spyOn(checkoutService, 'continueAsGuest').mockRejectedValue(error); const handleContinueAsGuest = jest.fn(); const component = mount( + onContinueAsGuest={handleContinueAsGuest} + viewType={CustomerViewType.Guest} + />, ); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - (component.find(GuestForm) as ReactWrapper) - .prop('onContinueAsGuest')({ - email: 'test@bigcommerce.com', - shouldSubscribe: true, - }); + (component.find(GuestForm) as ReactWrapper).prop('onContinueAsGuest')({ + email: 'test@bigcommerce.com', + shouldSubscribe: true, + }); + await new Promise((resolve) => process.nextTick(resolve)); - await new Promise(resolve => process.nextTick(resolve)); - - expect(handleContinueAsGuest) - .toHaveBeenCalled(); + expect(handleContinueAsGuest).toHaveBeenCalled(); }); it('renders CancellableEnforcedLogin if continue as guest fails with code 403', async () => { - jest.spyOn(checkoutService, 'continueAsGuest') - .mockRejectedValue({ status: 403, type: '' }); + jest.spyOn(checkoutService, 'continueAsGuest').mockRejectedValue({ + status: 403, + type: '', + }); const handleChangeViewType = jest.fn(); const component = mount( + onChangeViewType={handleChangeViewType} + viewType={CustomerViewType.Guest} + />, ); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - (component.find(GuestForm) as ReactWrapper) - .prop('onContinueAsGuest')({ - email: 'test@bigcommerce.com', - shouldSubscribe: false, - }); + (component.find(GuestForm) as ReactWrapper).prop('onContinueAsGuest')({ + email: 'test@bigcommerce.com', + shouldSubscribe: false, + }); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(handleChangeViewType).toHaveBeenCalledWith(CustomerViewType.CancellableEnforcedLogin); + expect(handleChangeViewType).toHaveBeenCalledWith( + CustomerViewType.CancellableEnforcedLogin, + ); }); it('renders SuggestedLogin form if continue as guest returns truthy shouldEncourageSignIn', async () => { - jest.spyOn(checkoutService.getState().data, 'getCustomer') - .mockReturnValue({ - ...getCustomer(), - isGuest: true, - shouldEncourageSignIn: true, - } as any); - - jest.spyOn(checkoutService, 'continueAsGuest') - .mockReturnValue(Promise.resolve(checkoutService.getState())); + jest.spyOn(checkoutService.getState().data, 'getCustomer').mockReturnValue({ + ...getCustomer(), + isGuest: true, + shouldEncourageSignIn: true, + } as any); + + jest.spyOn(checkoutService, 'continueAsGuest').mockReturnValue( + Promise.resolve(checkoutService.getState()), + ); const handleChangeViewType = jest.fn(); const component = mount( + onChangeViewType={handleChangeViewType} + viewType={CustomerViewType.Guest} + />, ); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - (component.find(GuestForm) as ReactWrapper) - .prop('onContinueAsGuest')({ - email: 'test@bigcommerce.com', - shouldSubscribe: false, - }); + (component.find(GuestForm) as ReactWrapper).prop('onContinueAsGuest')({ + email: 'test@bigcommerce.com', + shouldSubscribe: false, + }); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); expect(handleChangeViewType).toHaveBeenCalledWith(CustomerViewType.SuggestedLogin); @@ -360,366 +331,332 @@ describe('Customer', () => { }); it('renders EnforcedLogin form if continue as guest fails with code 429', async () => { - jest.spyOn(checkoutService, 'continueAsGuest') - .mockRejectedValue({ status: 429, type: '' }); + jest.spyOn(checkoutService, 'continueAsGuest').mockRejectedValue({ + status: 429, + type: '', + }); const handleChangeViewType = jest.fn(); const component = mount( + onChangeViewType={handleChangeViewType} + viewType={CustomerViewType.Guest} + />, ); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - (component.find(GuestForm) as ReactWrapper) - .prop('onContinueAsGuest')({ - email: 'test@bigcommerce.com', - shouldSubscribe: false, - }); + (component.find(GuestForm) as ReactWrapper).prop('onContinueAsGuest')({ + email: 'test@bigcommerce.com', + shouldSubscribe: false, + }); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); expect(handleChangeViewType).toHaveBeenCalledWith(CustomerViewType.EnforcedLogin); }); it('triggers error callback if customer is unable to continue as guest', async () => { - jest.spyOn(checkoutService, 'continueAsGuest') - .mockRejectedValue({ type: 'unknown_error' }); + jest.spyOn(checkoutService, 'continueAsGuest').mockRejectedValue({ + type: 'unknown_error', + }); const handleError = jest.fn(); const component = mount( + onContinueAsGuestError={handleError} + viewType={CustomerViewType.Guest} + />, ); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - (component.find(GuestForm) as ReactWrapper) - .prop('onContinueAsGuest')({ - email: 'test@bigcommerce.com', - shouldSubscribe: false, - }); + (component.find(GuestForm) as ReactWrapper).prop('onContinueAsGuest')({ + email: 'test@bigcommerce.com', + shouldSubscribe: false, + }); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(handleError) - .toHaveBeenCalled(); + expect(handleError).toHaveBeenCalled(); }); it('retains draft email address when switching to login view', async () => { - const component = mount( - - ); + const component = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - (component.find(GuestForm) as ReactWrapper) - .prop('onChangeEmail')('test@bigcommerce.com'); + (component.find(GuestForm) as ReactWrapper).prop('onChangeEmail')( + 'test@bigcommerce.com', + ); component.setProps({ viewType: CustomerViewType.Login }); component.update(); - expect((component.find(LoginForm) as ReactWrapper).prop('email')) - .toEqual('test@bigcommerce.com'); + expect((component.find(LoginForm) as ReactWrapper).prop('email')).toBe( + 'test@bigcommerce.com', + ); }); }); describe('when view type is "login"', () => { it('matches snapshot', async () => { const component = mount( - + , ); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(component.render()) - .toMatchSnapshot(); + expect(component.render()).toMatchSnapshot(); }); it('renders login form', async () => { - const component = mount( - - ); + const component = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(component.find(LoginForm).exists()) - .toEqual(true); + expect(component.find(LoginForm).exists()).toBe(true); }); it('renders sign-in email when link is clicked', async () => { const component = mount( - ); + , + ); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(component.find(EmailLoginForm).exists()) - .toEqual(false); + expect(component.find(EmailLoginForm).exists()).toBe(false); component.find('[data-test="customer-signin-link"]').simulate('click'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(component.find(EmailLoginForm).prop('emailHasBeenRequested')) - .toEqual(false); + expect(component.find(EmailLoginForm).prop('emailHasBeenRequested')).toBe(false); }); it('does not render sign-in email link when is embedded checkout', async () => { const component = mount( ); + isEmbedded={true} + isSignInEmailEnabled={true} + viewType={CustomerViewType.Login} + />, + ); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(component.find(EmailLoginForm).exists()) - .toEqual(false); + expect(component.find(EmailLoginForm).exists()).toBe(false); - expect(component.find('[data-test="customer-signin-link"]').exists()) - .toEqual(false); + expect(component.find('[data-test="customer-signin-link"]').exists()).toBe(false); }); it('passes data to login form', async () => { - const component = mount( - - ); + const component = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(component.find(LoginForm).props()) - .toMatchObject({ - email: billingAddress.email, - canCancel: config.checkoutSettings.guestCheckoutEnabled, - forgotPasswordUrl: config.links.forgotPasswordLink, - }); + expect(component.find(LoginForm).props()).toMatchObject({ + email: billingAddress.email, + canCancel: config.checkoutSettings.guestCheckoutEnabled, + forgotPasswordUrl: config.links.forgotPasswordLink, + }); }); it('handles "sign in" event', async () => { - jest.spyOn(checkoutService, 'signInCustomer') - .mockReturnValue(Promise.resolve(checkoutService.getState())); - - const component = mount( - + jest.spyOn(checkoutService, 'signInCustomer').mockReturnValue( + Promise.resolve(checkoutService.getState()), ); - await new Promise(resolve => process.nextTick(resolve)); + const component = mount(); + + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - (component.find(LoginForm) as ReactWrapper) - .prop('onSignIn')({ - email: 'test@bigcommerce.com', - password: 'password1', - }); + (component.find(LoginForm) as ReactWrapper).prop('onSignIn')({ + email: 'test@bigcommerce.com', + password: 'password1', + }); - expect(checkoutService.signInCustomer) - .toHaveBeenCalledWith({ - email: 'test@bigcommerce.com', - password: 'password1', - }); + expect(checkoutService.signInCustomer).toHaveBeenCalledWith({ + email: 'test@bigcommerce.com', + password: 'password1', + }); }); it('triggers completion callback if customer is successfully signed in', async () => { - jest.spyOn(checkoutService, 'signInCustomer') - .mockReturnValue(Promise.resolve(checkoutService.getState())); + jest.spyOn(checkoutService, 'signInCustomer').mockReturnValue( + Promise.resolve(checkoutService.getState()), + ); const handleSignedIn = jest.fn(); const component = mount( - + , ); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - (component.find(LoginForm) as ReactWrapper) - .prop('onSignIn')({ - email: 'test@bigcommerce.com', - password: 'password1', - }); + (component.find(LoginForm) as ReactWrapper).prop('onSignIn')({ + email: 'test@bigcommerce.com', + password: 'password1', + }); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(handleSignedIn) - .toHaveBeenCalled(); + expect(handleSignedIn).toHaveBeenCalled(); }); it('triggers error callback if customer is unable to sign in', async () => { - jest.spyOn(checkoutService, 'signInCustomer') - .mockRejectedValue({ type: 'unknown_error' }); + jest.spyOn(checkoutService, 'signInCustomer').mockRejectedValue({ + type: 'unknown_error', + }); const handleError = jest.fn(); const component = mount( - + , ); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - (component.find(LoginForm) as ReactWrapper) - .prop('onSignIn')({ - email: 'test@bigcommerce.com', - password: 'password1', - }); + (component.find(LoginForm) as ReactWrapper).prop('onSignIn')({ + email: 'test@bigcommerce.com', + password: 'password1', + }); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(handleError) - .toHaveBeenCalled(); + expect(handleError).toHaveBeenCalled(); }); it('clears error when "cancel" event is triggered', async () => { const error = new Error(); - jest.spyOn(checkoutService.getState().errors, 'getSignInError') - .mockReturnValue(error); + jest.spyOn(checkoutService.getState().errors, 'getSignInError').mockReturnValue(error); - jest.spyOn(checkoutService, 'clearError') - .mockReturnValue(Promise.resolve(checkoutService.getState())); - - const component = mount( - + jest.spyOn(checkoutService, 'clearError').mockReturnValue( + Promise.resolve(checkoutService.getState()), ); - await new Promise(resolve => process.nextTick(resolve)); + const component = mount(); + + await new Promise((resolve) => process.nextTick(resolve)); component.update(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (component.find(LoginForm) as ReactWrapper) - .prop('onCancel')!(); + (component.find(LoginForm) as ReactWrapper).prop('onCancel')!(); - expect(checkoutService.clearError) - .toHaveBeenCalledWith(error); + expect(checkoutService.clearError).toHaveBeenCalledWith(error); }); it('changes to guest view when "cancel" event is triggered', async () => { const handleChangeViewType = jest.fn(); const component = mount( + onChangeViewType={handleChangeViewType} + viewType={CustomerViewType.Login} + />, ); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (component.find(LoginForm) as ReactWrapper) - .prop('onCancel')!(); + (component.find(LoginForm) as ReactWrapper).prop('onCancel')!(); - expect(handleChangeViewType) - .toHaveBeenCalledWith(CustomerViewType.Guest); + expect(handleChangeViewType).toHaveBeenCalledWith(CustomerViewType.Guest); }); it('retains draft email address when switching to guest view', async () => { - const component = mount( - - ); + const component = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (component.find(LoginForm) as ReactWrapper) - .prop('onChangeEmail')!('test@bigcommerce.com'); + (component.find(LoginForm) as ReactWrapper).prop('onChangeEmail')!( + 'test@bigcommerce.com', + ); component.setProps({ viewType: CustomerViewType.Guest }); component.update(); - expect((component.find(GuestForm) as ReactWrapper).prop('email')) - .toEqual('test@bigcommerce.com'); + expect((component.find(GuestForm) as ReactWrapper).prop('email')).toBe( + 'test@bigcommerce.com', + ); }); }); describe('when view type is "cancellable_enforced_login"', () => { it('renders login form', async () => { const component = mount( - + , ); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(component.find(LoginForm).prop('viewType')) - .toEqual(CustomerViewType.CancellableEnforcedLogin); + expect(component.find(LoginForm).prop('viewType')).toEqual( + CustomerViewType.CancellableEnforcedLogin, + ); }); }); describe('when view type is "suggested_login"', () => { it('renders login form', async () => { - const component = mount( - - ); + const component = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(component.find(LoginForm).prop('viewType')) - .toEqual(CustomerViewType.SuggestedLogin); + expect(component.find(LoginForm).prop('viewType')).toEqual( + CustomerViewType.SuggestedLogin, + ); }); }); describe('when view type is "enforced_login"', () => { it('renders login form', async () => { - const component = mount( - - ); + const component = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(component.find(LoginForm).prop('viewType')) - .toEqual(CustomerViewType.EnforcedLogin); + expect(component.find(LoginForm).prop('viewType')).toEqual( + CustomerViewType.EnforcedLogin, + ); }); - it('calls sendLoginEmail and renders form when sign-in email link is clicked ', async () => { - const sendLoginEmail = jest.fn(() => new Promise(resolve => resolve()) as any); + it('calls sendLoginEmail and renders form when sign-in email link is clicked', async () => { + const sendLoginEmail = jest.fn(() => new Promise((resolve) => resolve()) as any); const component = mount( ); + isSignInEmailEnabled={true} + sendLoginEmail={sendLoginEmail} + viewType={CustomerViewType.EnforcedLogin} + />, + ); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); component.find('[data-test="customer-signin-link"]').simulate('click'); component.update(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(sendLoginEmail) - .toHaveBeenCalledWith({ email: 'foo@bar.com' }); - expect(component.find(EmailLoginForm).prop('emailHasBeenRequested')) - .toEqual(true); + expect(sendLoginEmail).toHaveBeenCalledWith({ email: 'foo@bar.com' }); + expect(component.find(EmailLoginForm).prop('emailHasBeenRequested')).toBe(true); }); it('renders EmailLoginForm even when sendLoginForm is rejected', async () => { @@ -727,23 +664,22 @@ describe('Customer', () => { const component = mount( ); + isSignInEmailEnabled={true} + sendLoginEmail={sendLoginEmail} + viewType={CustomerViewType.EnforcedLogin} + />, + ); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); component.find('[data-test="customer-signin-link"]').simulate('click'); component.update(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(sendLoginEmail) - .toHaveBeenCalledWith({ email: 'foo@bar.com' }); - expect(component.find(EmailLoginForm).prop('emailHasBeenRequested')) - .toEqual(true); + expect(sendLoginEmail).toHaveBeenCalledWith({ email: 'foo@bar.com' }); + expect(component.find(EmailLoginForm).prop('emailHasBeenRequested')).toBe(true); }); }); }); diff --git a/packages/core/src/app/customer/Customer.tsx b/packages/core/src/app/customer/Customer.tsx index bf14fdeeca..2fc414795d 100644 --- a/packages/core/src/app/customer/Customer.tsx +++ b/packages/core/src/app/customer/Customer.tsx @@ -1,19 +1,30 @@ -import { CheckoutSelectors, CustomerAccountRequestBody, CustomerCredentials, CustomerInitializeOptions, CustomerRequestOptions, ExecutePaymentMethodCheckoutOptions, FormField, GuestCredentials, SignInEmail, StoreConfig } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CustomerAccountRequestBody, + CustomerCredentials, + CustomerInitializeOptions, + CustomerRequestOptions, + ExecutePaymentMethodCheckoutOptions, + FormField, + GuestCredentials, + SignInEmail, + StoreConfig, +} from '@bigcommerce/checkout-sdk'; import { noop } from 'lodash'; import React, { Component, ReactNode } from 'react'; -import { withCheckout, CheckoutContextProps } from '../checkout'; +import { CheckoutContextProps, withCheckout } from '../checkout'; import { isErrorWithType } from '../common/error'; import { LoadingOverlay } from '../ui/loading'; -import { CreateAccountFormValues } from './getCreateCustomerValidationSchema'; -import mapCreateAccountFromFormValues from './mapCreateAccountFromFormValues'; import CheckoutButtonList from './CheckoutButtonList'; import CreateAccountForm from './CreateAccountForm'; import CustomerViewType from './CustomerViewType'; import EmailLoginForm, { EmailLoginFormValues } from './EmailLoginForm'; +import { CreateAccountFormValues } from './getCreateCustomerValidationSchema'; import GuestForm, { GuestFormValues } from './GuestForm'; import LoginForm from './LoginForm'; +import mapCreateAccountFromFormValues from './mapCreateAccountFromFormValues'; export interface CustomerProps { viewType: CustomerViewType; @@ -56,7 +67,9 @@ export interface WithCheckoutCustomerProps { clearError(error: Error): Promise; continueAsGuest(credentials: GuestCredentials): Promise; deinitializeCustomer(options: CustomerRequestOptions): Promise; - executePaymentMethodCheckout(options: ExecutePaymentMethodCheckoutOptions): Promise; + executePaymentMethodCheckout( + options: ExecutePaymentMethodCheckoutOptions, + ): Promise; initializeCustomer(options: CustomerInitializeOptions): Promise; sendLoginEmail(params: { email: string }): Promise; signIn(credentials: CustomerCredentials): Promise; @@ -122,14 +135,11 @@ class Customer extends Component - { isEmailLoginFormOpen && this.renderEmailLoginLinkForm() } - { shouldRenderLoginForm && this.renderLoginForm() } - { shouldRenderGuestForm && this.renderGuestForm() } - { shouldRenderCreateAccountForm && this.renderCreateAccountForm() } + + {isEmailLoginFormOpen && this.renderEmailLoginLinkForm()} + {shouldRenderLoginForm && this.renderLoginForm()} + {shouldRenderGuestForm && this.renderGuestForm()} + {shouldRenderCreateAccountForm && this.renderCreateAccountForm()} ); } @@ -153,52 +163,47 @@ class Customer extends Component } - continueAsGuestButtonLabelId={ 'customer.continue' } - defaultShouldSubscribe={ defaultShouldSubscribe } - email={ this.draftEmail || email } - isLoading={ isContinuingAsGuest || isInitializing || isExecutingPaymentMethodCheckout } - onChangeEmail={ this.handleChangeEmail } - onContinueAsGuest={ this.handleContinueAsGuest } - onShowLogin={ this.handleShowLogin } - privacyPolicyUrl={ privacyPolicyUrl } - requiresMarketingConsent={ requiresMarketingConsent } + continueAsGuestButtonLabelId="customer.continue" + defaultShouldSubscribe={defaultShouldSubscribe} + email={this.draftEmail || email} + isLoading={ + isContinuingAsGuest || isInitializing || isExecutingPaymentMethodCheckout + } + onChangeEmail={this.handleChangeEmail} + onContinueAsGuest={this.handleContinueAsGuest} + onShowLogin={this.handleShowLogin} + privacyPolicyUrl={privacyPolicyUrl} + requiresMarketingConsent={requiresMarketingConsent} /> ); } private renderEmailLoginLinkForm(): ReactNode { - const { - isEmailLoginFormOpen, - hasRequestedLoginEmail, - } = this.state; + const { isEmailLoginFormOpen, hasRequestedLoginEmail } = this.state; - const { - isSendingSignInEmail, - signInEmailError, - signInEmail, - } = this.props; + const { isSendingSignInEmail, signInEmailError, signInEmail } = this.props; return ( ); } @@ -220,12 +225,12 @@ class Customer extends Component ); } @@ -247,22 +252,26 @@ class Customer extends Component ); } @@ -281,10 +290,10 @@ class Customer extends Component Promise = async values => { - const { - sendLoginEmail, - } = this.props; + private handleSendLoginEmail: (values: EmailLoginFormValues) => Promise = async ( + values, + ) => { + const { sendLoginEmail } = this.props; try { await sendLoginEmail(values); @@ -295,7 +304,9 @@ class Customer extends Component Promise = async formValues => { + private handleContinueAsGuest: (formValues: GuestFormValues) => Promise = async ( + formValues, + ) => { const { canSubscribe, continueAsGuest, @@ -305,24 +316,30 @@ class Customer extends Component Promise = async credentials => { - const { - signIn, - onSignIn = noop, - onSignInError = noop, - } = this.props; + private handleSignIn: (credentials: CustomerCredentials) => Promise = async ( + credentials, + ) => { + const { signIn, onSignIn = noop, onSignInError = noop } = this.props; try { await signIn(credentials); @@ -357,11 +372,8 @@ class Customer extends Component void = async values => { - const { - createAccount = noop, - onAccountCreated = noop, - } = this.props; + private handleCreateAccount: (values: CreateAccountFormValues) => void = async (values) => { + const { createAccount = noop, onAccountCreated = noop } = this.props; await createAccount(mapCreateAccountFromFormValues(values)); @@ -369,19 +381,13 @@ class Customer extends Component void = () => { - const { - onChangeViewType = noop, - } = this.props; + const { onChangeViewType = noop } = this.props; onChangeViewType(CustomerViewType.CreateAccount); }; private handleCancelCreateAccount: () => void = () => { - const { - clearError, - onChangeViewType = noop, - createAccountError, - } = this.props; + const { clearError, onChangeViewType = noop, createAccountError } = this.props; if (createAccountError) { clearError(createAccountError); @@ -391,11 +397,7 @@ class Customer extends Component void = () => { - const { - clearError, - onChangeViewType = noop, - signInError, - } = this.props; + const { clearError, onChangeViewType = noop, signInError } = this.props; if (signInError) { clearError(signInError); @@ -404,7 +406,7 @@ class Customer extends Component void = email => { + private handleChangeEmail: (email: string) => void = (email) => { this.draftEmail = email; }; @@ -422,20 +424,38 @@ class Customer extends Component { let CustomerInfoTest: FunctionComponent; @@ -20,133 +24,117 @@ describe('CustomerInfo', () => { checkoutService = createCheckoutService(); checkoutState = checkoutService.getState(); - jest.spyOn(checkoutState.data, 'getBillingAddress') - .mockReturnValue(getBillingAddress()); + jest.spyOn(checkoutState.data, 'getBillingAddress').mockReturnValue(getBillingAddress()); - jest.spyOn(checkoutState.data, 'getCheckout') - .mockReturnValue(getCheckout()); + jest.spyOn(checkoutState.data, 'getCheckout').mockReturnValue(getCheckout()); - jest.spyOn(checkoutState.data, 'getCustomer') - .mockReturnValue(getGuestCustomer()); + jest.spyOn(checkoutState.data, 'getCustomer').mockReturnValue(getGuestCustomer()); - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - CustomerInfoTest = props => ( - - - + CustomerInfoTest = (props) => ( + + + ); }); it('matches snapshot', () => { - expect(render()) - .toMatchSnapshot(); + expect(render()).toMatchSnapshot(); }); describe('when customer is guest', () => { - it('displays billing address\'s email', () => { + it("displays billing address's email", () => { const component = mount(); - expect(component.find('[data-test="customer-info"]').text()) - .toEqual(getBillingAddress().email); + expect(component.find('[data-test="customer-info"]').text()).toEqual( + getBillingAddress().email, + ); }); it('does not render sign-out button', () => { const component = mount(); - expect(component.exists('[testId="sign-out-link"]')) - .toEqual(false); + expect(component.exists('[testId="sign-out-link"]')).toBe(false); }); }); describe('when customer is signed in', () => { beforeEach(() => { - jest.spyOn(checkoutState.data, 'getCustomer') - .mockReturnValue(getCustomer()); + jest.spyOn(checkoutState.data, 'getCustomer').mockReturnValue(getCustomer()); }); - it('displays customer\'s email', () => { + it("displays customer's email", () => { const component = mount(); - expect(component.find('[data-test="customer-info"]').text()) - .toEqual(getCustomer().email); + expect(component.find('[data-test="customer-info"]').text()).toEqual( + getCustomer().email, + ); }); it('renders sign-out button if customer can sign out', () => { const component = mount(); - expect(component.exists('[testId="sign-out-link"]')) - .toEqual(true); + expect(component.exists('[testId="sign-out-link"]')).toBe(true); }); it('signs out customer when they click on "sign out" button', () => { - jest.spyOn(checkoutService, 'signOutCustomer') - .mockReturnValue(Promise.resolve(checkoutService.getState())); + jest.spyOn(checkoutService, 'signOutCustomer').mockReturnValue( + Promise.resolve(checkoutService.getState()), + ); const component = mount(); - component.find('[data-test="sign-out-link"]') - .simulate('click'); + component.find('[data-test="sign-out-link"]').simulate('click'); - expect(checkoutService.signOutCustomer) - .toHaveBeenCalled(); + expect(checkoutService.signOutCustomer).toHaveBeenCalled(); }); it('triggers completion callback if able to sign out', async () => { - jest.spyOn(checkoutService, 'signOutCustomer') - .mockResolvedValue(checkoutService.getState()); + jest.spyOn(checkoutService, 'signOutCustomer').mockResolvedValue( + checkoutService.getState(), + ); const handleSignOut = jest.fn(); - const component = mount( - - ); + const component = mount(); - component.find('[data-test="sign-out-link"]') - .simulate('click'); + component.find('[data-test="sign-out-link"]').simulate('click'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(handleSignOut) - .toHaveBeenCalledWith({ isCartEmpty: false }); + expect(handleSignOut).toHaveBeenCalledWith({ isCartEmpty: false }); }); it('triggers completion callback if able to sign out but cart is empty', async () => { - jest.spyOn(checkoutService, 'signOutCustomer') - .mockRejectedValue({ type: 'checkout_not_available' }); + jest.spyOn(checkoutService, 'signOutCustomer').mockRejectedValue({ + type: 'checkout_not_available', + }); const handleSignOut = jest.fn(); - const component = mount( - - ); + const component = mount(); - component.find('[data-test="sign-out-link"]') - .simulate('click'); + component.find('[data-test="sign-out-link"]').simulate('click'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(handleSignOut) - .toHaveBeenCalledWith({ isCartEmpty: true }); + expect(handleSignOut).toHaveBeenCalledWith({ isCartEmpty: true }); }); it('triggers error callback if unable to sign out', async () => { - jest.spyOn(checkoutService, 'signOutCustomer') - .mockRejectedValue({ type: 'unknown_error' }); + jest.spyOn(checkoutService, 'signOutCustomer').mockRejectedValue({ + type: 'unknown_error', + }); const handleError = jest.fn(); - const component = mount( - - ); + const component = mount(); - component.find('[data-test="sign-out-link"]') - .simulate('click'); + component.find('[data-test="sign-out-link"]').simulate('click'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(handleError) - .toHaveBeenCalledWith({ type: 'unknown_error' }); + expect(handleError).toHaveBeenCalledWith({ type: 'unknown_error' }); }); }); }); diff --git a/packages/core/src/app/customer/CustomerInfo.tsx b/packages/core/src/app/customer/CustomerInfo.tsx index 5fd8bab4b2..52d81f4926 100644 --- a/packages/core/src/app/customer/CustomerInfo.tsx +++ b/packages/core/src/app/customer/CustomerInfo.tsx @@ -2,7 +2,7 @@ import { CheckoutSelectors, CustomerRequestOptions, CustomError } from '@bigcomm import { noop } from 'lodash'; import React, { FunctionComponent } from 'react'; -import { withCheckout, CheckoutContextProps } from '../checkout'; +import { CheckoutContextProps, withCheckout } from '../checkout'; import { isErrorWithType } from '../common/error'; import { TranslatedString } from '../locale'; import { Button, ButtonSize, ButtonVariant } from '../ui/button'; @@ -55,35 +55,35 @@ const CustomerInfo: FunctionComponent +
    - { email } + {email}
    - { isSignedIn && } + {isSignedIn && ( + + )}
    ); }; -function mapToWithCheckoutCustomerInfoProps( - { checkoutService, checkoutState }: CheckoutContextProps -): WithCheckoutCustomerInfoProps | null { +function mapToWithCheckoutCustomerInfoProps({ + checkoutService, + checkoutState, +}: CheckoutContextProps): WithCheckoutCustomerInfoProps | null { const { data: { getBillingAddress, getCheckout, getCustomer }, statuses: { isSigningOut }, @@ -97,7 +97,8 @@ function mapToWithCheckoutCustomerInfoProps( return null; } - const methodId = checkout.payments && checkout.payments.length === 1 ? checkout.payments[0].providerId : ''; + const methodId = + checkout.payments && checkout.payments.length === 1 ? checkout.payments[0].providerId : ''; return { email: billingAddress.email || customer.email, diff --git a/packages/core/src/app/customer/EmailField.tsx b/packages/core/src/app/customer/EmailField.tsx index 14f72383d4..b2b362e807 100644 --- a/packages/core/src/app/customer/EmailField.tsx +++ b/packages/core/src/app/customer/EmailField.tsx @@ -1,5 +1,5 @@ import { FieldProps } from 'formik'; -import React, { memo, useCallback, useMemo, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo, useCallback, useMemo } from 'react'; import { TranslatedString } from '../locale'; import { FormField, TextInput } from '../ui/form'; @@ -8,28 +8,29 @@ export interface EmailFieldProps { onChange?(value: string): void; } -const EmailField: FunctionComponent = ({ - onChange, -}) => { - const renderInput = useCallback((props: FieldProps) => ( - - ), []); +const EmailField: FunctionComponent = ({ onChange }) => { + const renderInput = useCallback( + (props: FieldProps) => ( + + ), + [], + ); - const labelContent = useMemo(() => ( - - ), []); + const labelContent = useMemo(() => , []); - return ; + return ( + + ); }; export default memo(EmailField); diff --git a/packages/core/src/app/customer/EmailLoginForm.spec.tsx b/packages/core/src/app/customer/EmailLoginForm.spec.tsx index 9940423149..ef57dbeac3 100644 --- a/packages/core/src/app/customer/EmailLoginForm.spec.tsx +++ b/packages/core/src/app/customer/EmailLoginForm.spec.tsx @@ -2,7 +2,13 @@ import { mount } from 'enzyme'; import React, { FunctionComponent } from 'react'; import { getStoreConfig } from '../config/config.mock'; -import { createLocaleContext, LocaleContext, LocaleContextType, TranslatedHtml, TranslatedString } from '../locale'; +import { + createLocaleContext, + LocaleContext, + LocaleContextType, + TranslatedHtml, + TranslatedString, +} from '../locale'; import { Alert, AlertType } from '../ui/alert'; import { Form } from '../ui/form'; import { LoadingSpinner } from '../ui/loading'; @@ -18,231 +24,207 @@ describe('EmailLoginForm', () => { beforeEach(() => { localeContext = createLocaleContext(getStoreConfig()); - EmailLoginFormTest = props => ( - - + EmailLoginFormTest = (props) => ( + + ); }); it('renders form', () => { - const component = mount(); + const component = mount(); - expect(component.find(Form).exists()) - .toEqual(true); + expect(component.find(Form).exists()).toBe(true); - expect(component.find('button[type="submit"]').text()) - .toEqual('Send'); + expect(component.find('button[type="submit"]').text()).toBe('Send'); - expect(component.find('button[type="button"]').text()) - .toEqual('Cancel'); + expect(component.find('button[type="button"]').text()).toBe('Cancel'); - expect(component.find(ModalHeader).find(TranslatedString).prop('id')) - .toEqual('login_email.header'); + expect(component.find(ModalHeader).find(TranslatedString).prop('id')).toBe( + 'login_email.header', + ); - expect(component.find('p').find(TranslatedString).prop('id')) - .toEqual('login_email.text'); + expect(component.find('p').find(TranslatedString).prop('id')).toBe('login_email.text'); - expect(component.find(LoadingSpinner).prop('isLoading')) - .toEqual(false); + expect(component.find(LoadingSpinner).prop('isLoading')).toBe(false); }); it('renders form with initial values', () => { - const component = mount( - - ); + const component = mount(); - expect(component.find(ModalHeader).find(TranslatedString).prop('id')) - .toEqual('login_email.header_with_email'); + expect(component.find(ModalHeader).find(TranslatedString).prop('id')).toBe( + 'login_email.header_with_email', + ); - expect(component.find('input[name="email"]').prop('value')) - .toEqual('test@bigcommerce.com'); + expect(component.find('input[name="email"]').prop('value')).toBe('test@bigcommerce.com'); - expect(component.find('button[type="submit"]').text()) - .toEqual('Send'); + expect(component.find('button[type="submit"]').text()).toBe('Send'); - expect(component.find('button[type="button"]').text()) - .toEqual('Cancel'); + expect(component.find('button[type="button"]').text()).toBe('Cancel'); }); it('notifies when user submits form', async () => { const handleSubmit = jest.fn(); const component = mount( - + , ); - component.find('input[name="email"]') + component + .find('input[name="email"]') .simulate('change', { target: { value: 'test@bigcommerce.com', name: 'email' } }); - component.find('form') - .simulate('submit'); + component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(handleSubmit) - .toHaveBeenCalled(); + expect(handleSubmit).toHaveBeenCalled(); }); it('renders "temporary disabled" if error.status === 429 (too many requests)', () => { const component = mount( + emailHasBeenRequested={true} + isOpen={true} + sentEmailError={{ status: 429 }} + />, ); - expect(component.find('[id="login_email.sent_text"]').exists()) - .toEqual(false); + expect(component.find('[id="login_email.sent_text"]').exists()).toBe(false); - expect(component.find(Alert).prop('type')) - .toEqual(AlertType.Error); + expect(component.find(Alert).prop('type')).toEqual(AlertType.Error); - expect(component.find(EmailField).exists()) - .toEqual(false); + expect(component.find(EmailField).exists()).toBe(false); - expect(component.find(Alert).find(TranslatedString).prop('id')) - .toEqual('login_email.error_temporary_disabled'); + expect(component.find(Alert).find(TranslatedString).prop('id')).toBe( + 'login_email.error_temporary_disabled', + ); - expect(component.find('button[type="submit"]').exists()) - .toEqual(false); + expect(component.find('button[type="submit"]').exists()).toBe(false); - expect(component.find(ModalHeader).find(TranslatedString).prop('id')) - .toEqual('common.error_heading'); + expect(component.find(ModalHeader).find(TranslatedString).prop('id')).toBe( + 'common.error_heading', + ); - expect(component.find('button[type="button"]').text()) - .toEqual('Ok'); + expect(component.find('button[type="button"]').text()).toBe('Ok'); }); it('renders "account not found" if error.status === 404', () => { const component = mount( + emailHasBeenRequested={true} + isOpen={true} + sentEmailError={{ status: 404 }} + />, ); - expect(component.find('[id="login_email.sent_text"]').exists()) - .toEqual(false); + expect(component.find('[id="login_email.sent_text"]').exists()).toBe(false); - expect(component.find(Alert).prop('type')) - .toEqual(AlertType.Error); + expect(component.find(Alert).prop('type')).toEqual(AlertType.Error); - expect(component.find(EmailField).exists()) - .toEqual(true); + expect(component.find(EmailField).exists()).toBe(true); - expect(component.find(Alert).find(TranslatedString).prop('id')) - .toEqual('login_email.error_not_found'); + expect(component.find(Alert).find(TranslatedString).prop('id')).toBe( + 'login_email.error_not_found', + ); - expect(component.find(ModalHeader).find(TranslatedString).prop('id')) - .toEqual('common.error_heading'); + expect(component.find(ModalHeader).find(TranslatedString).prop('id')).toBe( + 'common.error_heading', + ); }); it('renders "server error" if error.status === 500', () => { const component = mount( + emailHasBeenRequested={true} + isOpen={true} + sentEmailError={{ status: 500 }} + />, ); - expect(component.find('[id="login_email.sent_text"]').exists()) - .toEqual(false); + expect(component.find('[id="login_email.sent_text"]').exists()).toBe(false); - expect(component.find(EmailField).exists()) - .toEqual(true); + expect(component.find(EmailField).exists()).toBe(true); - expect(component.find(Alert).prop('type')) - .toEqual(AlertType.Error); + expect(component.find(Alert).prop('type')).toEqual(AlertType.Error); - expect(component.find(Alert).find(TranslatedString).prop('id')) - .toEqual('login_email.error_server'); + expect(component.find(Alert).find(TranslatedString).prop('id')).toBe( + 'login_email.error_server', + ); - expect(component.find(ModalHeader).find(TranslatedString).prop('id')) - .toEqual('common.error_heading'); + expect(component.find(ModalHeader).find(TranslatedString).prop('id')).toBe( + 'common.error_heading', + ); }); it('renders confirmation text if email has been requested', () => { const component = mount( + emailHasBeenRequested={true} + isOpen={true} + sentEmail={{ sent_email: 'sign_in', expiry: 890 }} + />, ); - expect(component.find('p').find(TranslatedHtml).props()) - .toEqual({ - id: 'login_email.sent_text', - data: { - email: 'foo@bar.com', - minutes: 15, - }, - }); + expect(component.find('p').find(TranslatedHtml).props()).toEqual({ + id: 'login_email.sent_text', + data: { + email: 'foo@bar.com', + minutes: 15, + }, + }); - expect(component.find(ModalHeader).find(TranslatedString).prop('id')) - .toEqual('login_email.sent_header'); + expect(component.find(ModalHeader).find(TranslatedString).prop('id')).toBe( + 'login_email.sent_header', + ); - expect(component.find(EmailField).exists()) - .toEqual(false); + expect(component.find(EmailField).exists()).toBe(false); - expect(component.find('form a').at(0).text()) - .toEqual('Resend link'); + expect(component.find('form a').at(0).text()).toBe('Resend link'); - expect(component.find('form a').at(1).text()) - .toEqual('sign in using your password'); + expect(component.find('form a').at(1).text()).toBe('sign in using your password'); }); it('renders reset email text if sent email is reset_password', () => { const component = mount( + emailHasBeenRequested={true} + isOpen={true} + sentEmail={{ sent_email: 'reset_password', expiry: 890 }} + />, ); - expect(component.find('p').find(TranslatedHtml).props()) - .toEqual(expect.objectContaining({ + expect(component.find('p').find(TranslatedHtml).props()).toEqual( + expect.objectContaining({ id: 'customer.reset_password_before_login_error', - })); + }), + ); - expect(component.find(ModalHeader).find(TranslatedString).prop('id')) - .toEqual('login_email.sent_header'); + expect(component.find(ModalHeader).find(TranslatedString).prop('id')).toBe( + 'login_email.sent_header', + ); - expect(component.find(EmailField).exists()) - .toEqual(false); + expect(component.find(EmailField).exists()).toBe(false); - expect(component.find('button[type="submit"]').exists()) - .toEqual(false); + expect(component.find('button[type="submit"]').exists()).toBe(false); }); it('displays error message if email is invalid', async () => { - const component = mount( - - ); + const component = mount(); - component.find('input[name="email"]') + component + .find('input[name="email"]') .simulate('change', { target: { value: 'test@bigcommerce.', name: 'email' } }); - component.find('form') - .simulate('submit'); + component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(component.find('[data-test="email-field-error-message"]').text()) - .toEqual('Email address must be valid'); + expect(component.find('[data-test="email-field-error-message"]').text()).toBe( + 'Email address must be valid', + ); }); }); diff --git a/packages/core/src/app/customer/EmailLoginForm.tsx b/packages/core/src/app/customer/EmailLoginForm.tsx index e8df8001c2..953e309bb7 100644 --- a/packages/core/src/app/customer/EmailLoginForm.tsx +++ b/packages/core/src/app/customer/EmailLoginForm.tsx @@ -1,17 +1,23 @@ import { SignInEmail } from '@bigcommerce/checkout-sdk'; -import { withFormik, FormikProps } from 'formik'; +import { FormikProps, withFormik } from 'formik'; import { noop } from 'lodash'; -import React, { memo, useMemo, FunctionComponent } from 'react'; - -import { withLanguage, TranslatedHtml, TranslatedLink, TranslatedString, WithLanguageProps } from '../locale'; +import React, { FunctionComponent, memo, useMemo } from 'react'; + +import { + TranslatedHtml, + TranslatedLink, + TranslatedString, + withLanguage, + WithLanguageProps, +} from '../locale'; import { Alert, AlertType } from '../ui/alert'; import { Button, ButtonVariant } from '../ui/button'; import { Form } from '../ui/form'; import { LoadingSpinner } from '../ui/loading'; import { Modal, ModalHeader } from '../ui/modal'; -import getEmailValidationSchema from './getEmailValidationSchema'; import EmailField from './EmailField'; +import getEmailValidationSchema from './getEmailValidationSchema'; export interface EmailLoginFormProps { email?: string; @@ -28,7 +34,9 @@ export interface EmailLoginFormValues { email: string; } -const EmailLoginForm: FunctionComponent> = ({ +const EmailLoginForm: FunctionComponent< + EmailLoginFormProps & WithLanguageProps & FormikProps +> = ({ email, isOpen, isSendingEmail = false, @@ -37,9 +45,7 @@ const EmailLoginForm: FunctionComponent { const modalHeaderStringId = useMemo(() => { if (emailHasBeenRequested) { @@ -57,13 +63,16 @@ const EmailLoginForm: FunctionComponent ( -
    - -
    - ), [onRequestClose]); + const okButton = useMemo( + () => ( +
    + +
    + ), + [onRequestClose], + ); const footer = useMemo(() => { if (sentEmailError && sentEmailError.status === 429) { @@ -81,14 +90,8 @@ const EmailLoginForm: FunctionComponent - - + +

    ); } @@ -97,16 +100,12 @@ const EmailLoginForm: FunctionComponent -
    @@ -129,13 +128,18 @@ const EmailLoginForm: FunctionComponent - { status === 429 ? - : - } + + {status === 429 ? ( + + ) : ( + + )} ); }, [sentEmailError]); @@ -151,13 +155,15 @@ const EmailLoginForm: FunctionComponent

    ); @@ -167,12 +173,14 @@ const EmailLoginForm: FunctionComponent; } - return (<> -

    - -

    - - ); + return ( + <> +

    + +

    + + + ); }, [sentEmailError, emailHasBeenRequested, sentEmail, formEmail]); return ( @@ -181,30 +189,32 @@ const EmailLoginForm: FunctionComponent - + } - isOpen={ isOpen } - onRequestClose={ onRequestClose } - shouldShowCloseButton={ true } + isOpen={isOpen} + onRequestClose={onRequestClose} + shouldShowCloseButton={true} >
    - - { error } - { form } - { footer } + + {error} + {form} + {footer} - ); + + ); }; -export default withLanguage(withFormik({ - mapPropsToValues: ({ - email = '', - }) => ({ - email, - }), - handleSubmit: (values, { props: { onSendLoginEmail = noop } }) => { - onSendLoginEmail(values); - }, - validationSchema: ({ language }: WithLanguageProps) => getEmailValidationSchema({ language }), -})(memo(EmailLoginForm))); +export default withLanguage( + withFormik({ + mapPropsToValues: ({ email = '' }) => ({ + email, + }), + handleSubmit: (values, { props: { onSendLoginEmail = noop } }) => { + onSendLoginEmail(values); + }, + validationSchema: ({ language }: WithLanguageProps) => + getEmailValidationSchema({ language }), + })(memo(EmailLoginForm)), +); diff --git a/packages/core/src/app/customer/GuestForm.spec.tsx b/packages/core/src/app/customer/GuestForm.spec.tsx index 0e298112be..9b352b0277 100644 --- a/packages/core/src/app/customer/GuestForm.spec.tsx +++ b/packages/core/src/app/customer/GuestForm.spec.tsx @@ -26,12 +26,9 @@ describe('GuestForm', () => { localeContext = createLocaleContext(getStoreConfig()); - TestComponent = props => ( - - + TestComponent = (props) => ( + + ); }); @@ -39,59 +36,46 @@ describe('GuestForm', () => { it('matches snapshot', () => { const component = render(); - expect(component) - .toMatchSnapshot(); + expect(component).toMatchSnapshot(); }); it('renders form with initial values', () => { const component = mount( - + , ); - expect(component.find('input[name="email"]').prop('value')) - .toEqual('test@bigcommerce.com'); + expect(component.find('input[name="email"]').prop('value')).toBe('test@bigcommerce.com'); - expect(component.find('input[name="shouldSubscribe"]').prop('value')) - .toEqual(true); + expect(component.find('input[name="shouldSubscribe"]').prop('value')).toBe(true); }); it('notifies when user clicks on "continue as guest" button', async () => { const handleContinueAsGuest = jest.fn(); - const component = mount( - - ); + const component = mount(); - component.find('input[name="email"]') + component + .find('input[name="email"]') .simulate('change', { target: { value: 'test@bigcommerce.com', name: 'email' } }); - component.find('input[name="shouldSubscribe"]') + component + .find('input[name="shouldSubscribe"]') .simulate('change', { target: { value: true, name: 'shouldSubscribe' } }); - component.find('form') - .simulate('submit'); + component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(handleContinueAsGuest) - .toHaveBeenCalledWith({ - email: 'test@bigcommerce.com', - privacyPolicy: false, - shouldSubscribe: true, - }); + expect(handleContinueAsGuest).toHaveBeenCalledWith({ + email: 'test@bigcommerce.com', + privacyPolicy: false, + shouldSubscribe: true, + }); }); it('disables "continue as guest" button when isLoading is true', () => { const handleContinueAsGuest = jest.fn(); const component = mount( - + , ); const button = component.find('[data-test="customer-continue-as-guest-button"]'); @@ -103,22 +87,19 @@ describe('GuestForm', () => { async function getEmailError(value: string): Promise { const handleContinueAsGuest = jest.fn(); const component = mount( - + , ); component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); return component.find('[data-test="email-field-error-message"]').text(); } - expect(getEmailError('')).resolves.toEqual('Email address is required'); + expect(getEmailError('')).resolves.toBe('Email address is required'); const invalidEmailMessage = 'Email address must be valid'; @@ -134,100 +115,70 @@ describe('GuestForm', () => { it('notifies when user clicks on "sign in" button', () => { const handleShowLogin = jest.fn(); - const component = mount( - - ); + const component = mount(); - component.find('[data-test="customer-continue-button"]') - .simulate('click'); + component.find('[data-test="customer-continue-button"]').simulate('click'); - expect(handleShowLogin) - .toHaveBeenCalled(); + expect(handleShowLogin).toHaveBeenCalled(); }); it('calls "onChangeEmail" handler when user changes email address', () => { const handleChangeEmail = jest.fn(); - const component = mount( - - ); + const component = mount(); - component.find('input[name="email"]') + component + .find('input[name="email"]') .simulate('change', { target: { value: 'test@bigcommerce', name: 'email' } }); - expect(handleChangeEmail) - .toHaveBeenCalledWith('test@bigcommerce'); + expect(handleChangeEmail).toHaveBeenCalledWith('test@bigcommerce'); }); it('calls "onChangeEmail" handler when user changes email address with strict validation enabled and it includes a domain', () => { const handleChangeEmail = jest.fn(); - const component = mount( - - ); + const component = mount(); - component.find('input[name="email"]') + component + .find('input[name="email"]') .simulate('change', { target: { value: 'test@bigcommerce.com', name: 'email' } }); - expect(handleChangeEmail) - .toHaveBeenCalledWith('test@bigcommerce.com'); + expect(handleChangeEmail).toHaveBeenCalledWith('test@bigcommerce.com'); }); it('renders newsletter field if store allows newsletter subscription', () => { - const component = mount( - - ); + const component = mount(); - expect(component.find('label[htmlFor="shouldSubscribe"]').text()) - .toEqual('Subscribe to our newsletter.'); - expect(component.exists('input[name="shouldSubscribe"]')) - .toEqual(true); + expect(component.find('label[htmlFor="shouldSubscribe"]').text()).toBe( + 'Subscribe to our newsletter.', + ); + expect(component.exists('input[name="shouldSubscribe"]')).toBe(true); }); it('renders marketing consent field', () => { const component = mount( - + , ); - expect(component.find('label[htmlFor="shouldSubscribe"]').text()) - .toEqual('I would like to receive updates and offers.'); - expect(component.exists('input[name="shouldSubscribe"]')) - .toEqual(true); + expect(component.find('label[htmlFor="shouldSubscribe"]').text()).toBe( + 'I would like to receive updates and offers.', + ); + expect(component.exists('input[name="shouldSubscribe"]')).toBe(true); }); it('sets newsletter field with default value', () => { const Container = ({ defaultShouldSubscribe }: { defaultShouldSubscribe: boolean }) => ( - + ); - const componentA = mount(); - const componentB = mount(); + const componentA = mount(); + const componentB = mount(); - expect(componentA.find('input[name="shouldSubscribe"]').prop('value')) - .toEqual(true); + expect(componentA.find('input[name="shouldSubscribe"]').prop('value')).toBe(true); - expect(componentB.find('input[name="shouldSubscribe"]').prop('value')) - .toEqual(false); + expect(componentB.find('input[name="shouldSubscribe"]').prop('value')).toBe(false); }); it('renders privacy policy field', () => { - const component = mount( - - ); + const component = mount(); expect(component.find(PrivacyPolicyField)).toHaveLength(1); }); @@ -235,46 +186,37 @@ describe('GuestForm', () => { it('displays error message if privacy policy is required and not checked', async () => { const handleContinueAsGuest = jest.fn(); const component = mount( - + , ); - component.find('input[name="email"]') + component + .find('input[name="email"]') .simulate('change', { target: { value: 'test@test.com', name: 'email' } }); - component.find('form') - .simulate('submit'); + component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(handleContinueAsGuest) - .not.toHaveBeenCalled(); + expect(handleContinueAsGuest).not.toHaveBeenCalled(); - expect(component.find('[data-test="privacy-policy-field-error-message"]').text()) - .toEqual('Please agree to the Privacy Policy.'); + expect(component.find('[data-test="privacy-policy-field-error-message"]').text()).toBe( + 'Please agree to the Privacy Policy.', + ); }); it('does not render "sign in" button when loading', () => { - const component = mount( - - ); + const component = mount(); - expect(component.find('[data-test="customer-continue-button"]').length).toEqual(0); + expect(component.find('[data-test="customer-continue-button"]')).toHaveLength(0); }); it('shows different action button label if another label id was provided', () => { - const component = mount( - - ); + const component = mount(); - expect(component.find('[data-test="customer-continue-button"]').text()).not.toEqual('Continue as guest'); + expect(component.find('[data-test="customer-continue-button"]').text()).not.toBe( + 'Continue as guest', + ); }); }); diff --git a/packages/core/src/app/customer/GuestForm.tsx b/packages/core/src/app/customer/GuestForm.tsx index c2ff46b189..b74c8cc273 100644 --- a/packages/core/src/app/customer/GuestForm.tsx +++ b/packages/core/src/app/customer/GuestForm.tsx @@ -1,11 +1,11 @@ -import { withFormik, FieldProps, FormikProps } from 'formik'; -import React, { memo, useCallback, FunctionComponent, ReactNode } from 'react'; +import { FieldProps, FormikProps, withFormik } from 'formik'; +import React, { FunctionComponent, memo, ReactNode, useCallback } from 'react'; import { object, string } from 'yup'; -import { withLanguage, TranslatedString, WithLanguageProps } from '../locale'; +import { TranslatedString, withLanguage, WithLanguageProps } from '../locale'; import { getPrivacyPolicyValidationSchema, PrivacyPolicyField } from '../privacyPolicy'; import { Button, ButtonVariant } from '../ui/button'; -import { BasicFormField, Fieldset, Form, Legend } from '../ui/form'; +import { BasicFormField, Fieldset, Form, Legend } from '../ui/form'; import EmailField from './EmailField'; import SubscribeField from './SubscribeField'; @@ -29,7 +29,9 @@ export interface GuestFormValues { shouldSubscribe: boolean; } -const GuestForm: FunctionComponent> = ({ +const GuestForm: FunctionComponent< + GuestFormProps & WithLanguageProps & FormikProps +> = ({ canSubscribe, checkoutButtons, continueAsGuestButtonLabelId, @@ -39,14 +41,12 @@ const GuestForm: FunctionComponent { - const renderField = useCallback((fieldProps: FieldProps) => ( - - ), [ - requiresMarketingConsent, - ]); + const renderField = useCallback( + (fieldProps: FieldProps) => ( + + ), + [requiresMarketingConsent], + ); return (
    - + - { (canSubscribe || requiresMarketingConsent) && } + {(canSubscribe || requiresMarketingConsent) && ( + + )} - { privacyPolicyUrl && } + {privacyPolicyUrl && }
    - { - !isLoading &&

    - - { ' ' } + {!isLoading && ( +

    + {' '}

    - } + )} - { checkoutButtons } + {checkoutButtons}
    ); }; -export default withLanguage(withFormik({ - mapPropsToValues: ({ - email = '', - defaultShouldSubscribe = false, - requiresMarketingConsent, - }) => ({ - email, - shouldSubscribe: requiresMarketingConsent ? false : defaultShouldSubscribe, - privacyPolicy: false, - }), - handleSubmit: (values, { props: { onContinueAsGuest } }) => { - onContinueAsGuest(values); - }, - validationSchema: ({ language, privacyPolicyUrl }: GuestFormProps & WithLanguageProps) => { - const email = string() - .email(language.translate('customer.email_invalid_error')) - .max(256) - .required(language.translate('customer.email_required_error')); +export default withLanguage( + withFormik({ + mapPropsToValues: ({ + email = '', + defaultShouldSubscribe = false, + requiresMarketingConsent, + }) => ({ + email, + shouldSubscribe: requiresMarketingConsent ? false : defaultShouldSubscribe, + privacyPolicy: false, + }), + handleSubmit: (values, { props: { onContinueAsGuest } }) => { + onContinueAsGuest(values); + }, + validationSchema: ({ language, privacyPolicyUrl }: GuestFormProps & WithLanguageProps) => { + const email = string() + .email(language.translate('customer.email_invalid_error')) + .max(256) + .required(language.translate('customer.email_required_error')); - const baseSchema = object({ email }); + const baseSchema = object({ email }); - if (privacyPolicyUrl) { - return baseSchema.concat(getPrivacyPolicyValidationSchema({ - isRequired: !!privacyPolicyUrl, - language, - })); - } + if (privacyPolicyUrl) { + return baseSchema.concat( + getPrivacyPolicyValidationSchema({ + isRequired: !!privacyPolicyUrl, + language, + }), + ); + } - return baseSchema; - }, -})(memo(GuestForm))); + return baseSchema; + }, + })(memo(GuestForm)), +); diff --git a/packages/core/src/app/customer/LoginForm.spec.tsx b/packages/core/src/app/customer/LoginForm.spec.tsx index ce31185322..ef6edd72a8 100644 --- a/packages/core/src/app/customer/LoginForm.spec.tsx +++ b/packages/core/src/app/customer/LoginForm.spec.tsx @@ -2,7 +2,13 @@ import { mount, render } from 'enzyme'; import React, { FunctionComponent } from 'react'; import { getStoreConfig } from '../config/config.mock'; -import { createLocaleContext, LocaleContext, LocaleContextType, TranslatedHtml, TranslatedLink } from '../locale'; +import { + createLocaleContext, + LocaleContext, + LocaleContextType, + TranslatedHtml, + TranslatedLink, +} from '../locale'; import { Alert } from '../ui/alert'; import CustomerViewType from './CustomerViewType'; @@ -22,68 +28,55 @@ describe('LoginForm', () => { localeContext = createLocaleContext(getStoreConfig()); - TestComponent = props => ( - - + TestComponent = (props) => ( + + ); }); it('matches snapshot', () => { - const component = render( - - ); + const component = render(); - expect(component) - .toMatchSnapshot(); + expect(component).toMatchSnapshot(); }); it('renders form with initial values', () => { - const component = mount( - - ); + const component = mount(); - expect(component.find('input[name="email"]').prop('value')) - .toEqual('test@bigcommerce.com'); + expect(component.find('input[name="email"]').prop('value')).toBe('test@bigcommerce.com'); }); it('notifies when user clicks on "sign in" button', async () => { const handleSignIn = jest.fn(); - const component = mount( - - ); + const component = mount(); - component.find('input[name="email"]') + component + .find('input[name="email"]') .simulate('change', { target: { value: 'test@bigcommerce.com', name: 'email' } }); - component.find('input[name="password"]') + component + .find('input[name="password"]') .simulate('change', { target: { value: 'password1', name: 'password' } }); - component.find('form') - .simulate('submit'); + component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(handleSignIn) - .toHaveBeenCalled(); + expect(handleSignIn).toHaveBeenCalled(); }); it('displays error message if email is not valid', () => { async function getEmailError(value: string): Promise { - const component = mount( - - ); + const component = mount(); - component.find('input[name="email"]') + component + .find('input[name="email"]') .simulate('change', { target: { value, name: 'email' } }); - component.find('form') - .simulate('submit'); + component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); @@ -99,26 +92,27 @@ describe('LoginForm', () => { expect(getEmailError('test@test.')).resolves.toEqual(invalidEmailMessage); expect(getEmailError('test@te st.test')).resolves.toEqual(invalidEmailMessage); expect(getEmailError('test@test.comtest@test.com')).resolves.toEqual(invalidEmailMessage); - expect(getEmailError('漢漢漢漢漢漢@漢漢漢漢漢漢.漢漢漢漢漢漢')).resolves.toEqual(invalidEmailMessage); + expect(getEmailError('漢漢漢漢漢漢@漢漢漢漢漢漢.漢漢漢漢漢漢')).resolves.toEqual( + invalidEmailMessage, + ); }); it('displays error message if password is missing', async () => { - const component = mount( - - ); + const component = mount(); - component.find('input[name="email"]') + component + .find('input[name="email"]') .simulate('change', { target: { value: 'test@bigcommerce.com', name: 'email' } }); - component.find('form') - .simulate('submit'); + component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(component.find('[data-test="password-field-error-message"]').text()) - .toEqual('Password is required'); + expect(component.find('[data-test="password-field-error-message"]').text()).toBe( + 'Password is required', + ); }); it('renders SuggestedLogin (no email input, suggestion, continue as guest) and ignores canCancel flag', () => { @@ -127,31 +121,27 @@ describe('LoginForm', () => { + onCancel={onCancel} + viewType={CustomerViewType.SuggestedLogin} + />, ); - expect(component.find(Alert).prop('type')) - .toEqual('info'); + expect(component.find(Alert).prop('type')).toBe('info'); - expect(component.find(Alert).find(TranslatedHtml).props()) - .toEqual({ - id: 'customer.guest_could_login', - data: { email: 'foo@bar.com' }, - }); + expect(component.find(Alert).find(TranslatedHtml).props()).toEqual({ + id: 'customer.guest_could_login', + data: { email: 'foo@bar.com' }, + }); - expect(component.exists('[data-test="customer-cancel-button"]')) - .toEqual(false); + expect(component.exists('[data-test="customer-cancel-button"]')).toBe(false); - expect(component.exists('[data-test="customer-guest-continue"]')) - .toEqual(true); + expect(component.exists('[data-test="customer-guest-continue"]')).toBe(true); component.find('[data-test="change-email"]').simulate('click'); + expect(onCancel).toHaveBeenCalled(); - expect(component.exists('input[name="email"]')) - .toEqual(false); + expect(component.exists('input[name="email"]')).toBe(false); }); it('renders info alert if CancellableEnforcedLogin, and hides email input', () => { @@ -159,21 +149,18 @@ describe('LoginForm', () => { + viewType={CustomerViewType.CancellableEnforcedLogin} + />, ); - expect(component.find(Alert).prop('type')) - .toEqual('info'); + expect(component.find(Alert).prop('type')).toBe('info'); - expect(component.find(Alert).find(TranslatedHtml).props()) - .toEqual({ - id: 'customer.guest_must_login', - data: { email: 'foo@bar.com' }, - }); + expect(component.find(Alert).find(TranslatedHtml).props()).toEqual({ + id: 'customer.guest_must_login', + data: { email: 'foo@bar.com' }, + }); - expect(component.exists('input[name="email"]')) - .toEqual(false); + expect(component.exists('input[name="email"]')).toBe(false); }); it('renders guest is temporary disabled alert if EnforcedLogin, and ignores canCancel flag', () => { @@ -181,75 +168,63 @@ describe('LoginForm', () => { + viewType={CustomerViewType.EnforcedLogin} + />, ); - expect(component.find(Alert).prop('type')) - .toEqual('error'); + expect(component.find(Alert).prop('type')).toBe('error'); - expect(component.find(Alert).find(TranslatedLink).prop('id')) - .toEqual('customer.guest_temporary_disabled'); + expect(component.find(Alert).find(TranslatedLink).prop('id')).toBe( + 'customer.guest_temporary_disabled', + ); - expect(component.exists('input[name="email"]')) - .toEqual(true); + expect(component.exists('input[name="email"]')).toBe(true); - expect(component.exists('[data-test="customer-cancel-button"]')) - .toEqual(false); + expect(component.exists('[data-test="customer-cancel-button"]')).toBe(false); }); it('renders error as alert if password is incorrect', () => { const error = Object.assign(new Error(), { body: { type: 'invalid login' } }); - const component = mount( - - ); + const component = mount(); - expect(component.find('[data-test="customer-login-error-message"]').text()) - .toEqual('The email or password you entered is not valid.'); + expect(component.find('[data-test="customer-login-error-message"]').text()).toBe( + 'The email or password you entered is not valid.', + ); }); it('renders cancel button if able to cancel', () => { - const component = mount( - - ); + const component = mount(); - expect(component.exists('[data-test="customer-cancel-button"]')) - .toEqual(true); + expect(component.exists('[data-test="customer-cancel-button"]')).toBe(true); }); it('does not render cancel button by default', () => { - const component = mount( - - ); + const component = mount(); - expect(component.exists('[data-test="customer-cancel-button"]')) - .toEqual(false); + expect(component.exists('[data-test="customer-cancel-button"]')).toBe(false); }); it('notifies when user changes email address', () => { const handleChangeEmail = jest.fn(); - const component = mount( - - ); + const component = mount(); - component.find('input[name="email"]') + component + .find('input[name="email"]') .simulate('change', { target: { value: 'test@bigcommerce.com', name: 'email' } }); - expect(handleChangeEmail) - .toHaveBeenCalledWith('test@bigcommerce.com'); + expect(handleChangeEmail).toHaveBeenCalledWith('test@bigcommerce.com'); }); it('shows different "Continue as guest" button label if another label id was provided', () => { const component = mount( + viewType={CustomerViewType.SuggestedLogin} + />, ); - expect(component.find('[data-test="customer-guest-continue"]').text()).not.toEqual('Continue as guest'); + expect(component.find('[data-test="customer-guest-continue"]').text()).not.toBe( + 'Continue as guest', + ); }); }); diff --git a/packages/core/src/app/customer/LoginForm.tsx b/packages/core/src/app/customer/LoginForm.tsx index 9ee32ee8e7..19c9c1869a 100644 --- a/packages/core/src/app/customer/LoginForm.tsx +++ b/packages/core/src/app/customer/LoginForm.tsx @@ -1,18 +1,24 @@ -import { withFormik, FormikProps } from 'formik'; +import { FormikProps, withFormik } from 'formik'; import { noop } from 'lodash'; -import React, { memo, useCallback, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo, useCallback } from 'react'; import { object, string } from 'yup'; import { preventDefault } from '../common/dom'; -import { withLanguage, TranslatedHtml, TranslatedLink, TranslatedString, WithLanguageProps } from '../locale'; +import { + TranslatedHtml, + TranslatedLink, + TranslatedString, + withLanguage, + WithLanguageProps, +} from '../locale'; import { Alert, AlertType } from '../ui/alert'; import { Button, ButtonVariant } from '../ui/button'; import { Fieldset, Form, Legend } from '../ui/form'; -import getEmailValidationSchema from './getEmailValidationSchema'; -import mapErrorMessage from './mapErrorMessage'; import CustomerViewType from './CustomerViewType'; import EmailField from './EmailField'; +import getEmailValidationSchema from './getEmailValidationSchema'; +import mapErrorMessage from './mapErrorMessage'; import PasswordField from './PasswordField'; export interface LoginFormProps { @@ -41,7 +47,9 @@ export interface LoginFormValues { password: string; } -const LoginForm: FunctionComponent> = ({ +const LoginForm: FunctionComponent< + LoginFormProps & WithLanguageProps & FormikProps +> = ({ canCancel, continueAsGuestButtonLabelId, forgotPasswordUrl, @@ -66,9 +74,9 @@ const LoginForm: FunctionComponent

    @@ -81,45 +89,44 @@ const LoginForm: FunctionComponent -
    - { viewType === CustomerViewType.SuggestedLogin && changeEmailLink() } + {viewType === CustomerViewType.SuggestedLogin && changeEmailLink()} - ); + + ); }; -export default withLanguage(withFormik({ - mapPropsToValues: ({ - email = '', - }) => ({ - email, - password: '', - }), - handleSubmit: (values, { props: { onSignIn } }) => { - onSignIn(values); - }, - validationSchema: ({ language }: LoginFormProps & WithLanguageProps) => - getEmailValidationSchema({ language }).concat(object({ - password: string() - .required(language.translate('customer.password_required_error')), - })), -})(memo(LoginForm))); +export default withLanguage( + withFormik({ + mapPropsToValues: ({ email = '' }) => ({ + email, + password: '', + }), + handleSubmit: (values, { props: { onSignIn } }) => { + onSignIn(values); + }, + validationSchema: ({ language }: LoginFormProps & WithLanguageProps) => + getEmailValidationSchema({ language }).concat( + object({ + password: string().required( + language.translate('customer.password_required_error'), + ), + }), + ), + })(memo(LoginForm)), +); diff --git a/packages/core/src/app/customer/PasswordField.tsx b/packages/core/src/app/customer/PasswordField.tsx index 9e4ee89ca9..e22f426f3d 100644 --- a/packages/core/src/app/customer/PasswordField.tsx +++ b/packages/core/src/app/customer/PasswordField.tsx @@ -1,5 +1,5 @@ import { FieldProps } from 'formik'; -import React, { memo, useCallback, useMemo, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo, useCallback, useMemo } from 'react'; import { TranslatedString } from '../locale'; import { FormField, TextInput } from '../ui/form'; @@ -7,33 +7,34 @@ import { IconEye, IconEyeSlash } from '../ui/icon'; import { Toggle } from '../ui/toggle'; const PasswordField: FunctionComponent = () => { - const renderInput = useCallback((props: FieldProps) => ( - - { ({ isOpen, toggle }) => ( - - ) } - - ), []); + const renderInput = useCallback( + (props: FieldProps) => ( + + {({ isOpen, toggle }) => ( + + )} + + ), + [], + ); - const labelContent = useMemo(() => ( - - ), []); + const labelContent = useMemo(() => , []); - return ; + return ; }; export default memo(PasswordField); diff --git a/packages/core/src/app/customer/SubscribeField.tsx b/packages/core/src/app/customer/SubscribeField.tsx index 532c4f19bd..f1652a82d3 100644 --- a/packages/core/src/app/customer/SubscribeField.tsx +++ b/packages/core/src/app/customer/SubscribeField.tsx @@ -1,5 +1,5 @@ import { FieldProps } from 'formik'; -import React, { memo, Fragment, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo } from 'react'; import { TranslatedString } from '../locale'; import { Input, Label } from '../ui/form'; @@ -8,23 +8,29 @@ export type SubscribeFieldProps = FieldProps & { requiresMarketingConsent: boolean; }; -const SubscribeField: FunctionComponent = ({ field, requiresMarketingConsent }) => ( - +const SubscribeField: FunctionComponent = ({ + field, + requiresMarketingConsent, +}) => ( + <> - - + ); export default memo(SubscribeField); diff --git a/packages/core/src/app/customer/canSignOut.spec.ts b/packages/core/src/app/customer/canSignOut.spec.ts index 7da20ffe3c..30b1410b2f 100644 --- a/packages/core/src/app/customer/canSignOut.spec.ts +++ b/packages/core/src/app/customer/canSignOut.spec.ts @@ -8,39 +8,34 @@ describe('canSignOut()', () => { const customer = getGuestCustomer(); const checkout = getCheckout(); - expect(canSignOut(customer, checkout, '')) - .toEqual(false); + expect(canSignOut(customer, checkout, '')).toBe(false); }); it('returns true if customer is signed in', () => { const customer = getCustomer(); const checkout = getCheckout(); - expect(canSignOut(customer, checkout, '')) - .toEqual(true); + expect(canSignOut(customer, checkout, '')).toBe(true); }); it('returns false if customer started with payment method that does not allow sign-out', () => { const customer = getCustomer(); const checkout = getCheckoutWithPayments(); - expect(canSignOut(customer, checkout, '')) - .toEqual(false); + expect(canSignOut(customer, checkout, '')).toBe(false); }); it('returns true if customer uses amazon as checkout method', () => { const customer = getCustomer(); const checkout = getCheckoutWithPayments(); - expect(canSignOut(customer, checkout, 'amazon')) - .toEqual(true); + expect(canSignOut(customer, checkout, 'amazon')).toBe(true); }); it('returns true if customer uses amazonpay as checkout method', () => { const customer = getCustomer(); const checkout = getCheckoutWithPayments(); - expect(canSignOut(customer, checkout, 'amazonpay')) - .toEqual(true); + expect(canSignOut(customer, checkout, 'amazonpay')).toBe(true); }); }); diff --git a/packages/core/src/app/customer/canSignOut.ts b/packages/core/src/app/customer/canSignOut.ts index aa5f9bfcd6..c974d361a8 100644 --- a/packages/core/src/app/customer/canSignOut.ts +++ b/packages/core/src/app/customer/canSignOut.ts @@ -3,16 +3,17 @@ import { every } from 'lodash'; import { SUPPORTED_METHODS } from './CheckoutButtonList'; -const SUPPORTED_SIGNOUT_METHODS = [ - 'amazon', - 'amazonpay', -]; +const SUPPORTED_SIGNOUT_METHODS = ['amazon', 'amazonpay']; export const isSupportedSignoutMethod = (methodId: string): boolean => { return SUPPORTED_SIGNOUT_METHODS.indexOf(methodId) > -1; }; -export default function canSignOut(customer: Customer, checkout: Checkout, methodId: string): boolean { +export default function canSignOut( + customer: Customer, + checkout: Checkout, + methodId: string, +): boolean { if (isSupportedSignoutMethod(methodId)) { return true; } @@ -22,7 +23,8 @@ export default function canSignOut(customer: Customer, checkout: Checkout, metho } // Return false if payment method offers its own checkout button - return every(checkout.payments, payment => - SUPPORTED_METHODS.indexOf(payment.providerId) === -1 + return every( + checkout.payments, + (payment) => SUPPORTED_METHODS.indexOf(payment.providerId) === -1, ); } diff --git a/packages/core/src/app/customer/checkoutSuggestion/BoltCheckoutSuggestion.spec.tsx b/packages/core/src/app/customer/checkoutSuggestion/BoltCheckoutSuggestion.spec.tsx index f3628be522..df12b195b1 100644 --- a/packages/core/src/app/customer/checkoutSuggestion/BoltCheckoutSuggestion.spec.tsx +++ b/packages/core/src/app/customer/checkoutSuggestion/BoltCheckoutSuggestion.spec.tsx @@ -18,12 +18,7 @@ describe('BoltCheckoutSuggestion', () => { methodId: 'bolt', }; - TestComponent = props => ( - - ); + TestComponent = (props) => ; }); it('deinitializes previous Bolt customer strategy before initialisation', () => { @@ -48,7 +43,9 @@ describe('BoltCheckoutSuggestion', () => { }); it('calls onUnhandledError if initialization was failed', () => { - defaultProps.initializeCustomer = jest.fn(() => { throw new Error(); }); + defaultProps.initializeCustomer = jest.fn(() => { + throw new Error(); + }); mount(); @@ -64,7 +61,7 @@ describe('BoltCheckoutSuggestion', () => { initializeOptions.bolt.onInit(customerHasBoltAccount); }); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); expect(component.find('[data-test="suggestion-action-button"]')).toHaveLength(0); @@ -79,7 +76,7 @@ describe('BoltCheckoutSuggestion', () => { initializeOptions.bolt.onInit(customerHasBoltAccount); }); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); expect(component.find('[data-test="suggestion-action-button"]')).toHaveLength(1); @@ -92,7 +89,7 @@ describe('BoltCheckoutSuggestion', () => { act(() => initializeOptions.bolt.onInit(customerHasBoltAccount)); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); const actionButton = component.find('[data-test="suggestion-action-button"]'); diff --git a/packages/core/src/app/customer/checkoutSuggestion/BoltCheckoutSuggestion.tsx b/packages/core/src/app/customer/checkoutSuggestion/BoltCheckoutSuggestion.tsx index 1bd9086789..5d2c9c007a 100644 --- a/packages/core/src/app/customer/checkoutSuggestion/BoltCheckoutSuggestion.tsx +++ b/packages/core/src/app/customer/checkoutSuggestion/BoltCheckoutSuggestion.tsx @@ -1,6 +1,11 @@ -import { CheckoutSelectors, CustomerInitializeOptions, CustomerRequestOptions, ExecutePaymentMethodCheckoutOptions } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CustomerInitializeOptions, + CustomerRequestOptions, + ExecutePaymentMethodCheckoutOptions, +} from '@bigcommerce/checkout-sdk'; import { noop } from 'lodash'; -import React, { memo, useEffect, useState, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo, useEffect, useState } from 'react'; import { stopPropagation } from '../../common/dom'; import { TranslatedString } from '../../locale'; @@ -11,7 +16,9 @@ export interface BoltCheckoutSuggestionProps { isExecutingPaymentMethodCheckout: boolean; methodId: string; deinitializeCustomer(options: CustomerRequestOptions): Promise; - executePaymentMethodCheckout(options: ExecutePaymentMethodCheckoutOptions): Promise; + executePaymentMethodCheckout( + options: ExecutePaymentMethodCheckoutOptions, + ): Promise; initializeCustomer(options: CustomerInitializeOptions): Promise; onUnhandledError?(error: Error): void; } @@ -24,7 +31,7 @@ const BoltCheckoutSuggestion: FunctionComponent = ( initializeCustomer, onUnhandledError = noop, }) => { - const [ showSuggestion, setShowSuggestion ] = useState(false); + const [showSuggestion, setShowSuggestion] = useState(false); useEffect(() => { deinitializeCustomer({ methodId }); @@ -33,7 +40,7 @@ const BoltCheckoutSuggestion: FunctionComponent = ( initializeCustomer({ methodId, bolt: { - onInit: hasBoltAccount => { + onInit: (hasBoltAccount) => { setShowSuggestion(hasBoltAccount); }, }, @@ -56,30 +63,25 @@ const BoltCheckoutSuggestion: FunctionComponent = ( }; return ( -
    +

    diff --git a/packages/core/src/app/customer/checkoutSuggestion/CheckoutSuggestion.spec.tsx b/packages/core/src/app/customer/checkoutSuggestion/CheckoutSuggestion.spec.tsx index 7bd0312a9c..423ca2382f 100644 --- a/packages/core/src/app/customer/checkoutSuggestion/CheckoutSuggestion.spec.tsx +++ b/packages/core/src/app/customer/checkoutSuggestion/CheckoutSuggestion.spec.tsx @@ -1,4 +1,8 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, +} from '@bigcommerce/checkout-sdk'; import { mount, render } from 'enzyme'; import React, { FunctionComponent } from 'react'; @@ -8,7 +12,10 @@ import { getStoreConfig } from '../../config/config.mock'; import { LocaleProvider } from '../../locale'; import BoltCheckoutSuggestion from './BoltCheckoutSuggestion'; -import CheckoutSuggestion, { CheckoutSuggestionProps, WithCheckoutSuggestionsProps } from './CheckoutSuggestion'; +import CheckoutSuggestion, { + CheckoutSuggestionProps, + WithCheckoutSuggestionsProps, +} from './CheckoutSuggestion'; describe('CheckoutSuggestion', () => { let defaultProps: WithCheckoutSuggestionsProps & CheckoutSuggestionProps; @@ -29,40 +36,41 @@ describe('CheckoutSuggestion', () => { checkoutService = createCheckoutService(); checkoutState = checkoutService.getState(); - jest.spyOn(checkoutState.data, 'getCheckout') - .mockReturnValue(getCheckout()); + jest.spyOn(checkoutState.data, 'getCheckout').mockReturnValue(getCheckout()); - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - TestComponent = props => ( - - - + TestComponent = (props) => ( + + + ); }); it('does not render anything if method id is not provided', () => { - const component = render(); + const component = render(); - expect(component.html().length).toBe(0); + expect(component.html()).toHaveLength(0); }); it('initializes Bolt Checkout suggestion block', () => { - const container = mount(); + const container = mount( + , + ); const component = container.find(BoltCheckoutSuggestion); - expect(component.props()) - .toEqual(expect.objectContaining({ + expect(component.props()).toEqual( + expect.objectContaining({ deinitializeCustomer: expect.any(Function), executePaymentMethodCheckout: expect.any(Function), initializeCustomer: expect.any(Function), isExecutingPaymentMethodCheckout: false, methodId: 'bolt', onUnhandledError: expect.any(Function), - })); + }), + ); expect(defaultProps.initializeCustomer).toHaveBeenCalledWith({ methodId: 'bolt', diff --git a/packages/core/src/app/customer/checkoutSuggestion/CheckoutSuggestion.tsx b/packages/core/src/app/customer/checkoutSuggestion/CheckoutSuggestion.tsx index 3aaf54d53a..f5d3a948b2 100644 --- a/packages/core/src/app/customer/checkoutSuggestion/CheckoutSuggestion.tsx +++ b/packages/core/src/app/customer/checkoutSuggestion/CheckoutSuggestion.tsx @@ -1,7 +1,12 @@ -import { CheckoutSelectors, CustomerInitializeOptions, CustomerRequestOptions, ExecutePaymentMethodCheckoutOptions } from '@bigcommerce/checkout-sdk'; -import React, { memo, FunctionComponent } from 'react'; - -import { withCheckout, CheckoutContextProps } from '../../checkout'; +import { + CheckoutSelectors, + CustomerInitializeOptions, + CustomerRequestOptions, + ExecutePaymentMethodCheckoutOptions, +} from '@bigcommerce/checkout-sdk'; +import React, { FunctionComponent, memo } from 'react'; + +import { CheckoutContextProps, withCheckout } from '../../checkout'; import { PaymentMethodId } from '../../payment/paymentMethod'; import BoltCheckoutSuggestion from './BoltCheckoutSuggestion'; @@ -14,24 +19,26 @@ export interface WithCheckoutSuggestionsProps { isExecutingPaymentMethodCheckout: boolean; providerWithCustomCheckout?: string; deinitializeCustomer(options: CustomerRequestOptions): Promise; - executePaymentMethodCheckout(options: ExecutePaymentMethodCheckoutOptions): Promise; + executePaymentMethodCheckout( + options: ExecutePaymentMethodCheckoutOptions, + ): Promise; initializeCustomer(options: CustomerInitializeOptions): Promise; } -const CheckoutSuggestion: FunctionComponent = ({ - providerWithCustomCheckout, - ...rest -}) => { +const CheckoutSuggestion: FunctionComponent< + WithCheckoutSuggestionsProps & CheckoutSuggestionProps +> = ({ providerWithCustomCheckout, ...rest }) => { if (providerWithCustomCheckout === PaymentMethodId.Bolt) { - return ; + return ; } return null; }; -const mapToCheckoutSuggestionProps = ( - { checkoutService, checkoutState }: CheckoutContextProps -): WithCheckoutSuggestionsProps | null => { +const mapToCheckoutSuggestionProps = ({ + checkoutService, + checkoutState, +}: CheckoutContextProps): WithCheckoutSuggestionsProps | null => { const { data: { getCheckout, getConfig }, statuses: { isExecutingPaymentMethodCheckout }, diff --git a/packages/core/src/app/customer/customWalletButton/AmazonPayV2Button.spec.tsx b/packages/core/src/app/customer/customWalletButton/AmazonPayV2Button.spec.tsx index 0cdf2b3390..503e01bd65 100644 --- a/packages/core/src/app/customer/customWalletButton/AmazonPayV2Button.spec.tsx +++ b/packages/core/src/app/customer/customWalletButton/AmazonPayV2Button.spec.tsx @@ -10,12 +10,12 @@ describe('AmazonPayV2Button', () => { it('renders as CheckoutButton', () => { const container = shallow( + containerId="test" + deinitialize={noop} + initialize={noop} + methodId="amazonpay" + onError={noop} + />, ); expect(container.hasClass('AmazonPayContainer')).toBe(true); diff --git a/packages/core/src/app/customer/customWalletButton/AmazonPayV2Button.tsx b/packages/core/src/app/customer/customWalletButton/AmazonPayV2Button.tsx index 1ca6f2e2b8..50859c15ee 100644 --- a/packages/core/src/app/customer/customWalletButton/AmazonPayV2Button.tsx +++ b/packages/core/src/app/customer/customWalletButton/AmazonPayV2Button.tsx @@ -2,10 +2,10 @@ import React, { FunctionComponent } from 'react'; import CheckoutButton, { CheckoutButtonProps } from '../CheckoutButton'; -const AmazonPayV2Button: FunctionComponent = props => ( -
    - -
    +const AmazonPayV2Button: FunctionComponent = (props) => ( +
    + +
    ); export default AmazonPayV2Button; diff --git a/packages/core/src/app/customer/customWalletButton/ApplePayButton.spec.tsx b/packages/core/src/app/customer/customWalletButton/ApplePayButton.spec.tsx index bf7e4efe91..68abd8189c 100644 --- a/packages/core/src/app/customer/customWalletButton/ApplePayButton.spec.tsx +++ b/packages/core/src/app/customer/customWalletButton/ApplePayButton.spec.tsx @@ -18,13 +18,13 @@ describe('ApplePayButton', () => { beforeEach(() => { localeContext = createLocaleContext(getStoreConfig()); ButtonTest = () => ( - + ); @@ -33,11 +33,12 @@ describe('ApplePayButton', () => { it('renders as CheckoutButton', () => { const container = mount(); - expect(container.find(CheckoutButton).length).toEqual(1); + expect(container.find(CheckoutButton)).toHaveLength(1); }); it('initializes the button correctly', () => { mount(); + expect(initialize).toHaveBeenCalledWith({ methodId: 'applepay', applepay: { diff --git a/packages/core/src/app/customer/customWalletButton/ApplePayButton.tsx b/packages/core/src/app/customer/customWalletButton/ApplePayButton.tsx index 7a106b9cc5..0451e42e25 100644 --- a/packages/core/src/app/customer/customWalletButton/ApplePayButton.tsx +++ b/packages/core/src/app/customer/customWalletButton/ApplePayButton.tsx @@ -1,5 +1,5 @@ import { CustomerInitializeOptions } from '@bigcommerce/checkout-sdk'; -import React, { useCallback, useContext, FunctionComponent } from 'react'; +import React, { FunctionComponent, useCallback, useContext } from 'react'; import { navigateToOrderConfirmation } from '../../checkout'; import { LocaleContext } from '../../locale'; @@ -11,18 +11,22 @@ const ApplePayButton: FunctionComponent = ({ ...rest }) => { const localeContext = useContext(LocaleContext); - const initializeOptions = useCallback((options: CustomerInitializeOptions) => initialize({ - ...options, - applepay: { - container: rest.containerId, - shippingLabel: localeContext?.language.translate('cart.shipping_text'), - subtotalLabel: localeContext?.language.translate('cart.subtotal_text'), - onError, - onPaymentAuthorize: navigateToOrderConfirmation, - }, - }), [initialize, localeContext, onError, rest.containerId]); + const initializeOptions = useCallback( + (options: CustomerInitializeOptions) => + initialize({ + ...options, + applepay: { + container: rest.containerId, + shippingLabel: localeContext?.language.translate('cart.shipping_text'), + subtotalLabel: localeContext?.language.translate('cart.subtotal_text'), + onError, + onPaymentAuthorize: navigateToOrderConfirmation, + }, + }), + [initialize, localeContext, onError, rest.containerId], + ); - return ; + return ; }; export default ApplePayButton; diff --git a/packages/core/src/app/customer/getCreateCustomerValidationSchema.ts b/packages/core/src/app/customer/getCreateCustomerValidationSchema.ts index a1faf7fdf1..e5e9742ebd 100644 --- a/packages/core/src/app/customer/getCreateCustomerValidationSchema.ts +++ b/packages/core/src/app/customer/getCreateCustomerValidationSchema.ts @@ -1,8 +1,12 @@ import { FormField, LanguageService } from '@bigcommerce/checkout-sdk'; import { memoize } from '@bigcommerce/memoize'; -import { object, string, ObjectSchema } from 'yup'; +import { object, ObjectSchema, string } from 'yup'; -import { getCustomFormFieldsValidationSchema, CustomFormFieldValues, TranslateValidationErrorFunction } from '../formFields'; +import { + CustomFormFieldValues, + getCustomFormFieldsValidationSchema, + TranslateValidationErrorFunction, +} from '../formFields'; import getEmailValidationSchema from './getEmailValidationSchema'; import { PasswordRequirements } from './getPasswordRequirements'; @@ -22,7 +26,9 @@ export interface CreateCustomerValidationSchema { passwordRequirements: PasswordRequirements; } -function getTranslateCreateCustomerError(language?: LanguageService): TranslateValidationErrorFunction { +function getTranslateCreateCustomerError( + language?: LanguageService, +): TranslateValidationErrorFunction { return (type, { label, min, max }) => { if (!language) { return; @@ -43,8 +49,6 @@ function getTranslateCreateCustomerError(language?: LanguageService): TranslateV if (type === 'invalid') { return language.translate('customer.invalid_characters_error', { label }); } - - return; }; } @@ -54,18 +58,29 @@ export default memoize(function getCreateCustomerValidationSchema({ passwordRequirements: { description, numeric, alpha, minLength }, }: CreateCustomerValidationSchema): ObjectSchema { return object({ - firstName: string().required(language.translate('address.first_name_required_error')), - lastName: string().required(language.translate('address.last_name_required_error')), - password: string() - .required(language.translate('customer.password_required_error') || description) - .matches(numeric, language.translate('customer.password_number_required_error') || description) - .matches(alpha, language.translate('customer.password_letter_required_error') || description) - .min(minLength, language.translate('customer.password_under_minimum_length_error' || description)) - .max(100, language.translate('customer.password_over_maximum_length_error')), - }) + firstName: string().required(language.translate('address.first_name_required_error')), + lastName: string().required(language.translate('address.last_name_required_error')), + password: string() + .required(language.translate('customer.password_required_error') || description) + .matches( + numeric, + language.translate('customer.password_number_required_error') || description, + ) + .matches( + alpha, + language.translate('customer.password_letter_required_error') || description, + ) + .min( + minLength, + language.translate('customer.password_under_minimum_length_error' || description), + ) + .max(100, language.translate('customer.password_over_maximum_length_error')), + }) .concat(getEmailValidationSchema({ language })) - .concat(getCustomFormFieldsValidationSchema({ - formFields, - translate: getTranslateCreateCustomerError(language), - })); + .concat( + getCustomFormFieldsValidationSchema({ + formFields, + translate: getTranslateCreateCustomerError(language), + }), + ); }); diff --git a/packages/core/src/app/customer/getEmailValidationSchema.ts b/packages/core/src/app/customer/getEmailValidationSchema.ts index ed8a264be7..71863f0231 100644 --- a/packages/core/src/app/customer/getEmailValidationSchema.ts +++ b/packages/core/src/app/customer/getEmailValidationSchema.ts @@ -1,5 +1,5 @@ import { LanguageService } from '@bigcommerce/checkout-sdk'; -import { object, string, ObjectSchema } from 'yup'; +import { object, ObjectSchema, string } from 'yup'; import { EMAIL_REGEXP } from './validationPatterns'; diff --git a/packages/core/src/app/customer/getPasswordRequirements.ts b/packages/core/src/app/customer/getPasswordRequirements.ts index e67b87ac01..3b10472959 100644 --- a/packages/core/src/app/customer/getPasswordRequirements.ts +++ b/packages/core/src/app/customer/getPasswordRequirements.ts @@ -9,12 +9,7 @@ export interface PasswordRequirements { export function getPasswordRequirementsFromConfig(config: ShopperConfig): PasswordRequirements { const { - passwordRequirements: { - minlength, - error: description, - alpha, - numeric, - }, + passwordRequirements: { minlength, error: description, alpha, numeric }, } = config; return getPasswordRequirements({ diff --git a/packages/core/src/app/customer/index.ts b/packages/core/src/app/customer/index.ts index fbe1e479c0..85645a59d8 100644 --- a/packages/core/src/app/customer/index.ts +++ b/packages/core/src/app/customer/index.ts @@ -4,5 +4,9 @@ export { default as CustomerInfo, CustomerInfoProps, CustomerSignOutEvent } from export { default as CheckoutSuggestion } from './checkoutSuggestion/CheckoutSuggestion'; export { default as GuestForm, GuestFormProps, GuestFormValues } from './GuestForm'; export { default as LoginForm, LoginFormProps, LoginFormValues } from './LoginForm'; -export { default as getPasswordRequirements, getPasswordRequirementsFromConfig, PasswordRequirements } from './getPasswordRequirements'; +export { + default as getPasswordRequirements, + getPasswordRequirementsFromConfig, + PasswordRequirements, +} from './getPasswordRequirements'; export { SUPPORTED_METHODS } from './CheckoutButtonList'; diff --git a/packages/core/src/app/customer/mapCreateAccountFromFormValues.ts b/packages/core/src/app/customer/mapCreateAccountFromFormValues.ts index d1190b5f60..36d79e32d6 100644 --- a/packages/core/src/app/customer/mapCreateAccountFromFormValues.ts +++ b/packages/core/src/app/customer/mapCreateAccountFromFormValues.ts @@ -4,9 +4,11 @@ import { mapCustomFormFieldsFromFormValues } from '../formFields'; import { CreateAccountFormValues } from './getCreateCustomerValidationSchema'; -export default function mapCreateAccountFromFormValues( - { acceptsMarketingEmails, customFields, ...values }: CreateAccountFormValues -): CustomerAccountRequestBody { +export default function mapCreateAccountFromFormValues({ + acceptsMarketingEmails, + customFields, + ...values +}: CreateAccountFormValues): CustomerAccountRequestBody { return { ...values, acceptsMarketingEmails: acceptsMarketingEmails && acceptsMarketingEmails.length > 0, diff --git a/packages/core/src/app/customer/mapErrorMessage.spec.ts b/packages/core/src/app/customer/mapErrorMessage.spec.ts index f37268f3b2..2b24576118 100644 --- a/packages/core/src/app/customer/mapErrorMessage.spec.ts +++ b/packages/core/src/app/customer/mapErrorMessage.spec.ts @@ -4,30 +4,30 @@ describe('mapErrorMessage()', () => { let translate: (key: string) => string; beforeEach(() => { - translate = jest.fn(key => key); + translate = jest.fn((key) => key); }); it('returns translated throttled error message', () => { - expect(mapErrorMessage({ body: { type: 'throttled_login' } }, translate)) - .toEqual('customer.sign_in_throttled_error'); + expect(mapErrorMessage({ body: { type: 'throttled_login' } }, translate)).toBe( + 'customer.sign_in_throttled_error', + ); - expect(translate) - .toHaveBeenCalledWith('customer.sign_in_throttled_error'); + expect(translate).toHaveBeenCalledWith('customer.sign_in_throttled_error'); }); it('returns translated reset password error message', () => { - expect(mapErrorMessage({ body: { type: 'reset_password_before_login' } }, translate)) - .toEqual('customer.reset_password_before_login_error'); + expect(mapErrorMessage({ body: { type: 'reset_password_before_login' } }, translate)).toBe( + 'customer.reset_password_before_login_error', + ); - expect(translate) - .toHaveBeenCalledWith('customer.reset_password_before_login_error'); + expect(translate).toHaveBeenCalledWith('customer.reset_password_before_login_error'); }); it('returns translated sign in error message by default', () => { - expect(mapErrorMessage({ body: { type: 'some_unknown_error' } }, translate)) - .toEqual('customer.sign_in_error'); + expect(mapErrorMessage({ body: { type: 'some_unknown_error' } }, translate)).toBe( + 'customer.sign_in_error', + ); - expect(translate) - .toHaveBeenCalledWith('customer.sign_in_error'); + expect(translate).toHaveBeenCalledWith('customer.sign_in_error'); }); }); diff --git a/packages/core/src/app/customer/mapErrorMessage.ts b/packages/core/src/app/customer/mapErrorMessage.ts index 6445d86b7d..c9658b05d7 100644 --- a/packages/core/src/app/customer/mapErrorMessage.ts +++ b/packages/core/src/app/customer/mapErrorMessage.ts @@ -1,17 +1,17 @@ export default function mapErrorMessage( error: any, // TODO: Export `RequestError` - translate: (key: string) => string + translate: (key: string) => string, ): string { const type = error.body && error.body.type; switch (type) { - case 'throttled_login': - return translate('customer.sign_in_throttled_error'); + case 'throttled_login': + return translate('customer.sign_in_throttled_error'); - case 'reset_password_before_login': - return translate('customer.reset_password_before_login_error'); + case 'reset_password_before_login': + return translate('customer.reset_password_before_login_error'); - default: - return translate('customer.sign_in_error'); + default: + return translate('customer.sign_in_error'); } } diff --git a/packages/core/src/app/customer/resolveCheckoutButton.ts b/packages/core/src/app/customer/resolveCheckoutButton.ts index 9e9c257664..b56f35907d 100644 --- a/packages/core/src/app/customer/resolveCheckoutButton.ts +++ b/packages/core/src/app/customer/resolveCheckoutButton.ts @@ -1,11 +1,17 @@ -import { CheckoutButtonProps, CheckoutButtonResolveId } from '@bigcommerce/checkout/payment-integration-api'; import { ComponentType } from 'react'; +import { + CheckoutButtonProps, + CheckoutButtonResolveId, +} from '@bigcommerce/checkout/payment-integration-api'; + import { resolveComponent } from '../common/resolver'; -export default function resolveCheckoutButton(resolveId: CheckoutButtonResolveId): ComponentType | undefined { +export default function resolveCheckoutButton( + resolveId: CheckoutButtonResolveId, +): ComponentType | undefined { return resolveComponent( resolveId, - require('../generated/checkoutButtons') + require('../generated/checkoutButtons'), ); } diff --git a/packages/core/src/app/customer/validationPatterns.ts b/packages/core/src/app/customer/validationPatterns.ts index f9d5d0a3a4..0ddfa357d7 100644 --- a/packages/core/src/app/customer/validationPatterns.ts +++ b/packages/core/src/app/customer/validationPatterns.ts @@ -1,3 +1,4 @@ // NOTE: This is a legacy regex used to create accounts, more flexible than the current used one // we need to keep this regex for login validation as accounts might have been created using this regex -export const EMAIL_REGEXP = /^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; +export const EMAIL_REGEXP = + /^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; diff --git a/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutStyleParser.spec.ts b/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutStyleParser.spec.ts index adfd68efd8..93cf13236a 100644 --- a/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutStyleParser.spec.ts +++ b/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutStyleParser.spec.ts @@ -1,5 +1,5 @@ -import { styles } from './embeddedCheckoutStyles.mock'; import EmbeddedCheckoutStyleParser from './EmbeddedCheckoutStyleParser'; +import { styles } from './embeddedCheckoutStyles.mock'; describe('EmbeddedCheckoutStyleParser', () => { let parser: EmbeddedCheckoutStyleParser; diff --git a/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutStyleParser.ts b/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutStyleParser.ts index a2721234a6..47237fdeb6 100644 --- a/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutStyleParser.ts +++ b/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutStyleParser.ts @@ -35,185 +35,301 @@ export default class EmbeddedCheckoutStyleParser { if (styles.label) { rules.push(toCSSRule('.optimizedCheckout-form-label', styles.label)); - rules.push(toCSSRule( - '.form-field--error .optimizedCheckout-form-label', - styles.label, styles.label.error)); - rules.push(toCSSRule('.form-field--error .form-inlineMessage', styles.label, styles.label.error)); + rules.push( + toCSSRule( + '.form-field--error .optimizedCheckout-form-label', + styles.label, + styles.label.error, + ), + ); + rules.push( + toCSSRule( + '.form-field--error .form-inlineMessage', + styles.label, + styles.label.error, + ), + ); } if (styles.button) { rules.push(toCSSRule('.optimizedCheckout-buttonPrimary', styles.button)); - rules.push(toCSSRule('.optimizedCheckout-buttonPrimary:active', styles.button, styles.button.active)); - rules.push(toCSSRule('.optimizedCheckout-buttonPrimary:focus', styles.button, styles.button.focus)); - rules.push(toCSSRule('.optimizedCheckout-buttonPrimary:hover', styles.button, styles.button.hover)); - rules.push(toCSSRule( - '.optimizedCheckout-buttonPrimary[disabled]', - styles.button, - styles.button.disabled - )); - rules.push(toCSSRule( - '.optimizedCheckout-buttonPrimary[disabled]:active', - styles.button, - styles.button.disabled - )); - rules.push(toCSSRule( - '.optimizedCheckout-buttonPrimary[disabled]:focus', - styles.button, - styles.button.disabled - )); - rules.push(toCSSRule( - '.optimizedCheckout-buttonPrimary[disabled]:hover', - styles.button, - styles.button.disabled - )); - rules.push(toCSSRule( - '.optimizedCheckout-buttonPrimary[disabled].is-active', - styles.button, - styles.button.disabled - )); - rules.push(toCSSRule( - '.optimizedCheckout-buttonPrimary[disabled].is-loading', - styles.button, - styles.button.disabled - )); + rules.push( + toCSSRule( + '.optimizedCheckout-buttonPrimary:active', + styles.button, + styles.button.active, + ), + ); + rules.push( + toCSSRule( + '.optimizedCheckout-buttonPrimary:focus', + styles.button, + styles.button.focus, + ), + ); + rules.push( + toCSSRule( + '.optimizedCheckout-buttonPrimary:hover', + styles.button, + styles.button.hover, + ), + ); + rules.push( + toCSSRule( + '.optimizedCheckout-buttonPrimary[disabled]', + styles.button, + styles.button.disabled, + ), + ); + rules.push( + toCSSRule( + '.optimizedCheckout-buttonPrimary[disabled]:active', + styles.button, + styles.button.disabled, + ), + ); + rules.push( + toCSSRule( + '.optimizedCheckout-buttonPrimary[disabled]:focus', + styles.button, + styles.button.disabled, + ), + ); + rules.push( + toCSSRule( + '.optimizedCheckout-buttonPrimary[disabled]:hover', + styles.button, + styles.button.disabled, + ), + ); + rules.push( + toCSSRule( + '.optimizedCheckout-buttonPrimary[disabled].is-active', + styles.button, + styles.button.disabled, + ), + ); + rules.push( + toCSSRule( + '.optimizedCheckout-buttonPrimary[disabled].is-loading', + styles.button, + styles.button.disabled, + ), + ); } if (styles.secondaryButton) { - rules.push(toCSSRule( - '.optimizedCheckout-buttonSecondary', - styles.secondaryButton - )); - rules.push(toCSSRule( - '.optimizedCheckout-buttonSecondary:active', - styles.secondaryButton, - styles.secondaryButton.active - )); - rules.push(toCSSRule( - '.optimizedCheckout-buttonSecondary:focus', - styles.secondaryButton, - styles.secondaryButton.focus - )); - rules.push(toCSSRule( - '.optimizedCheckout-buttonSecondary:hover', - styles.secondaryButton, - styles.secondaryButton.hover - )); - rules.push(toCSSRule( - '.optimizedCheckout-buttonSecondary[disabled]', - styles.secondaryButton, - styles.secondaryButton.disabled - )); - rules.push(toCSSRule( - '.optimizedCheckout-buttonSecondary[disabled]:active', - styles.secondaryButton, - styles.secondaryButton.disabled - )); - rules.push(toCSSRule( - '.optimizedCheckout-buttonSecondary[disabled]:focus', - styles.secondaryButton, - styles.secondaryButton.disabled - )); - rules.push(toCSSRule( - '.optimizedCheckout-buttonSecondary[disabled]:hover', - styles.secondaryButton, - styles.secondaryButton.disabled - )); - rules.push(toCSSRule( - '.optimizedCheckout-buttonSecondary[disabled].is-active', - styles.secondaryButton, - styles.secondaryButton.disabled - )); - rules.push(toCSSRule( - '.optimizedCheckout-buttonSecondary[disabled].is-loading', - styles.secondaryButton, - styles.secondaryButton.disabled - )); + rules.push(toCSSRule('.optimizedCheckout-buttonSecondary', styles.secondaryButton)); + rules.push( + toCSSRule( + '.optimizedCheckout-buttonSecondary:active', + styles.secondaryButton, + styles.secondaryButton.active, + ), + ); + rules.push( + toCSSRule( + '.optimizedCheckout-buttonSecondary:focus', + styles.secondaryButton, + styles.secondaryButton.focus, + ), + ); + rules.push( + toCSSRule( + '.optimizedCheckout-buttonSecondary:hover', + styles.secondaryButton, + styles.secondaryButton.hover, + ), + ); + rules.push( + toCSSRule( + '.optimizedCheckout-buttonSecondary[disabled]', + styles.secondaryButton, + styles.secondaryButton.disabled, + ), + ); + rules.push( + toCSSRule( + '.optimizedCheckout-buttonSecondary[disabled]:active', + styles.secondaryButton, + styles.secondaryButton.disabled, + ), + ); + rules.push( + toCSSRule( + '.optimizedCheckout-buttonSecondary[disabled]:focus', + styles.secondaryButton, + styles.secondaryButton.disabled, + ), + ); + rules.push( + toCSSRule( + '.optimizedCheckout-buttonSecondary[disabled]:hover', + styles.secondaryButton, + styles.secondaryButton.disabled, + ), + ); + rules.push( + toCSSRule( + '.optimizedCheckout-buttonSecondary[disabled].is-active', + styles.secondaryButton, + styles.secondaryButton.disabled, + ), + ); + rules.push( + toCSSRule( + '.optimizedCheckout-buttonSecondary[disabled].is-loading', + styles.secondaryButton, + styles.secondaryButton.disabled, + ), + ); } if (styles.input) { rules.push(toCSSRule('.optimizedCheckout-form-input', styles.input)); - rules.push(toCSSRule('.optimizedCheckout-form-input:focus', styles.input, styles.input.focus)); - rules.push(toCSSRule('.optimizedCheckout-form-input:hover', styles.input, styles.input.hover)); - rules.push(toCSSRule( - '.optimizedCheckout-form-input::placeholder', - styles.input, - styles.input.placeholder - )); - rules.push(toCSSRule( - '.form-field--error .optimizedCheckout-form-input', - styles.input, - styles.input.error - )); - rules.push(toCSSRule( - '.form-field--error .optimizedCheckout-form-input:focus', - styles.input, - styles.input.error - )); - rules.push(toCSSRule( - '.form-field--error .optimizedCheckout-form-input:hover', - styles.input, - styles.input.error - )); + rules.push( + toCSSRule('.optimizedCheckout-form-input:focus', styles.input, styles.input.focus), + ); + rules.push( + toCSSRule('.optimizedCheckout-form-input:hover', styles.input, styles.input.hover), + ); + rules.push( + toCSSRule( + '.optimizedCheckout-form-input::placeholder', + styles.input, + styles.input.placeholder, + ), + ); + rules.push( + toCSSRule( + '.form-field--error .optimizedCheckout-form-input', + styles.input, + styles.input.error, + ), + ); + rules.push( + toCSSRule( + '.form-field--error .optimizedCheckout-form-input:focus', + styles.input, + styles.input.error, + ), + ); + rules.push( + toCSSRule( + '.form-field--error .optimizedCheckout-form-input:hover', + styles.input, + styles.input.error, + ), + ); } if (styles.select) { rules.push(toCSSRule('.optimizedCheckout-form-select', styles.select)); - rules.push(toCSSRule('.optimizedCheckout-form-select:focus', styles.select, styles.select.focus)); - rules.push(toCSSRule('.optimizedCheckout-form-select:hover', styles.select, styles.select.hover)); - rules.push(toCSSRule( - '.form-field--error .optimizedCheckout-form-select', - styles.select, - styles.select.error - )); - rules.push(toCSSRule( - '.form-field--error .optimizedCheckout-form-select:focus', - styles.select, - styles.select.error - )); - rules.push(toCSSRule( - '.form-field--error .optimizedCheckout-form-select:hover', - styles.select, - styles.select.error - )); + rules.push( + toCSSRule( + '.optimizedCheckout-form-select:focus', + styles.select, + styles.select.focus, + ), + ); + rules.push( + toCSSRule( + '.optimizedCheckout-form-select:hover', + styles.select, + styles.select.hover, + ), + ); + rules.push( + toCSSRule( + '.form-field--error .optimizedCheckout-form-select', + styles.select, + styles.select.error, + ), + ); + rules.push( + toCSSRule( + '.form-field--error .optimizedCheckout-form-select:focus', + styles.select, + styles.select.error, + ), + ); + rules.push( + toCSSRule( + '.form-field--error .optimizedCheckout-form-select:hover', + styles.select, + styles.select.error, + ), + ); } if (styles.checkbox) { rules.push(toCSSRule('.optimizedCheckout-form-checkbox', styles.checkbox)); - rules.push(toCSSRule('.optimizedCheckout-form-checkbox:focus', styles.checkbox, styles.checkbox.focus)); - rules.push(toCSSRule('.optimizedCheckout-form-checkbox:hover', styles.checkbox, styles.checkbox.hover)); - rules.push(toCSSRule( - '.form-field--error .optimizedCheckout-form-checkbox', - styles.checkbox, - styles.checkbox.error - )); - rules.push(toCSSRule( - '.form-field--error .optimizedCheckout-form-checkbox:focus', - styles.checkbox, - styles.checkbox.error - )); - rules.push(toCSSRule( - '.form-field--error .optimizedCheckout-form-checkbox:hover', - styles.checkbox, - styles.checkbox.error - )); + rules.push( + toCSSRule( + '.optimizedCheckout-form-checkbox:focus', + styles.checkbox, + styles.checkbox.focus, + ), + ); + rules.push( + toCSSRule( + '.optimizedCheckout-form-checkbox:hover', + styles.checkbox, + styles.checkbox.hover, + ), + ); + rules.push( + toCSSRule( + '.form-field--error .optimizedCheckout-form-checkbox', + styles.checkbox, + styles.checkbox.error, + ), + ); + rules.push( + toCSSRule( + '.form-field--error .optimizedCheckout-form-checkbox:focus', + styles.checkbox, + styles.checkbox.error, + ), + ); + rules.push( + toCSSRule( + '.form-field--error .optimizedCheckout-form-checkbox:hover', + styles.checkbox, + styles.checkbox.error, + ), + ); } if (styles.radio) { rules.push(toCSSRule('.optimizedCheckout-form-radio', styles.radio)); - rules.push(toCSSRule('.optimizedCheckout-form-radio:focus', styles.radio, styles.radio.focus)); - rules.push(toCSSRule('.optimizedCheckout-form-radio:hover', styles.radio, styles.radio.hover)); - rules.push(toCSSRule('.form-field--error .optimizedCheckout-form-radio', styles.radio, styles.radio.error - )); - rules.push(toCSSRule( - '.form-field--error .optimizedCheckout-form-radio:focus', - styles.radio, - styles.radio.error - )); - rules.push(toCSSRule( - '.form-field--error .optimizedCheckout-form-radio:hover', - styles.radio, - styles.radio.error - )); + rules.push( + toCSSRule('.optimizedCheckout-form-radio:focus', styles.radio, styles.radio.focus), + ); + rules.push( + toCSSRule('.optimizedCheckout-form-radio:hover', styles.radio, styles.radio.hover), + ); + rules.push( + toCSSRule( + '.form-field--error .optimizedCheckout-form-radio', + styles.radio, + styles.radio.error, + ), + ); + rules.push( + toCSSRule( + '.form-field--error .optimizedCheckout-form-radio:focus', + styles.radio, + styles.radio.error, + ), + ); + rules.push( + toCSSRule( + '.form-field--error .optimizedCheckout-form-radio:hover', + styles.radio, + styles.radio.error, + ), + ); } if (styles.step) { @@ -224,16 +340,20 @@ export default class EmbeddedCheckoutStyleParser { if (styles.checklist) { rules.push(toCSSRule('.optimizedCheckout-form-checklist', styles.checklist)); rules.push(toCSSRule('.optimizedCheckout-form-checklist-item', styles.checklist)); - rules.push(toCSSRule( - '.optimizedCheckout-form-checklist-item:hover', - styles.checklist, - styles.checklist.hover - )); - rules.push(toCSSRule( - '.optimizedCheckout-form-checklist-item--selected', - styles.checklist, - styles.checklist.checked - )); + rules.push( + toCSSRule( + '.optimizedCheckout-form-checklist-item:hover', + styles.checklist, + styles.checklist.hover, + ), + ); + rules.push( + toCSSRule( + '.optimizedCheckout-form-checklist-item--selected', + styles.checklist, + styles.checklist.checked, + ), + ); } if (styles.discountBanner) { @@ -246,7 +366,9 @@ export default class EmbeddedCheckoutStyleParser { if (styles.orderSummary) { rules.push(toCSSRule('.optimizedCheckout-orderSummary', styles.orderSummary)); - rules.push(toCSSRule('.optimizedCheckout-orderSummary-cartSection', styles.orderSummary)); + rules.push( + toCSSRule('.optimizedCheckout-orderSummary-cartSection', styles.orderSummary), + ); } return rules; diff --git a/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutStylesheet.spec.ts b/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutStylesheet.spec.ts index d99dccc999..d583dfa37e 100644 --- a/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutStylesheet.spec.ts +++ b/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutStylesheet.spec.ts @@ -1,7 +1,7 @@ import { EmbeddedCheckoutStyles } from '@bigcommerce/checkout-sdk'; -import EmbeddedCheckoutStylesheet from './EmbeddedCheckoutStylesheet'; import EmbeddedCheckoutStyleParser from './EmbeddedCheckoutStyleParser'; +import EmbeddedCheckoutStylesheet from './EmbeddedCheckoutStylesheet'; describe('EmbeddedCheckoutStylesheet', () => { let stylesheet: EmbeddedCheckoutStylesheet; @@ -27,7 +27,7 @@ describe('EmbeddedCheckoutStylesheet', () => { const tag = stylesheet.append(styles); const sheet = tag.sheet as CSSStyleSheet; - expect(sheet.cssRules[0].cssText).toEqual('body {background-color: #000;}'); + expect(sheet.cssRules[0].cssText).toBe('body {background-color: #000;}'); }); it('appends stylesheet tag to head', () => { diff --git a/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutStylesheet.ts b/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutStylesheet.ts index 4211828f10..f7adf9025c 100644 --- a/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutStylesheet.ts +++ b/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutStylesheet.ts @@ -7,9 +7,7 @@ import EmbeddedCheckoutStyleParser from './EmbeddedCheckoutStyleParser'; export default class EmbeddedCheckoutStylesheet { private _parser: EmbeddedCheckoutStyleParser; - constructor( - embeddedCheckoutStyleParser: EmbeddedCheckoutStyleParser - ) { + constructor(embeddedCheckoutStyleParser: EmbeddedCheckoutStyleParser) { this._parser = embeddedCheckoutStyleParser; } diff --git a/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutSupport.spec.ts b/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutSupport.spec.ts index 07e53ba0e9..232d39f2d1 100644 --- a/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutSupport.spec.ts +++ b/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutSupport.spec.ts @@ -1,7 +1,7 @@ import { createLanguageService } from '@bigcommerce/checkout-sdk'; -import { EmbeddedCheckoutUnsupportedError } from './errors'; import EmbeddedCheckoutSupport from './EmbeddedCheckoutSupport'; +import { EmbeddedCheckoutUnsupportedError } from './errors'; describe('EmbeddedCheckoutSupport', () => { let embeddedCheckoutSupport: EmbeddedCheckoutSupport; @@ -9,17 +9,19 @@ describe('EmbeddedCheckoutSupport', () => { beforeEach(() => { embeddedCheckoutSupport = new EmbeddedCheckoutSupport( ['foo', 'bar'], - createLanguageService() + createLanguageService(), ); }); it('throws error if one of methods is unsupported', () => { - expect(() => embeddedCheckoutSupport.isSupported('foo', 'hello')) - .toThrow(EmbeddedCheckoutUnsupportedError); + expect(() => embeddedCheckoutSupport.isSupported('foo', 'hello')).toThrow( + EmbeddedCheckoutUnsupportedError, + ); }); it('does not throw error if supported method is passed', () => { - expect(() => embeddedCheckoutSupport.isSupported('hello')) - .not.toThrow(EmbeddedCheckoutUnsupportedError); + expect(() => embeddedCheckoutSupport.isSupported('hello')).not.toThrow( + EmbeddedCheckoutUnsupportedError, + ); }); }); diff --git a/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutSupport.ts b/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutSupport.ts index bb71d16904..76d7252447 100644 --- a/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutSupport.ts +++ b/packages/core/src/app/embeddedCheckout/EmbeddedCheckoutSupport.ts @@ -5,15 +5,10 @@ import { CheckoutSupport } from '../checkout'; import { EmbeddedCheckoutUnsupportedError } from './errors'; export default class EmbeddedCheckoutSupport implements CheckoutSupport { - constructor( - private unsupportedMethods: string[], - private langService: LanguageService - ) {} + constructor(private unsupportedMethods: string[], private langService: LanguageService) {} isSupported(...ids: string[]): boolean { - const unsupportedMethods = ids.filter(id => - this.unsupportedMethods.indexOf(id) >= 0 - ); + const unsupportedMethods = ids.filter((id) => this.unsupportedMethods.indexOf(id) >= 0); if (unsupportedMethods.length === 0) { return true; @@ -22,7 +17,7 @@ export default class EmbeddedCheckoutSupport implements CheckoutSupport { throw new EmbeddedCheckoutUnsupportedError( this.langService.translate('embedded_checkout.unsupported_error', { methods: unsupportedMethods.join(', '), - }) + }), ); } } diff --git a/packages/core/src/app/embeddedCheckout/createEmbeddedCheckoutStylesheet.ts b/packages/core/src/app/embeddedCheckout/createEmbeddedCheckoutStylesheet.ts index e02d28a4d7..be7b33665f 100644 --- a/packages/core/src/app/embeddedCheckout/createEmbeddedCheckoutStylesheet.ts +++ b/packages/core/src/app/embeddedCheckout/createEmbeddedCheckoutStylesheet.ts @@ -1,5 +1,5 @@ -import EmbeddedCheckoutStylesheet from './EmbeddedCheckoutStylesheet'; import EmbeddedCheckoutStyleParser from './EmbeddedCheckoutStyleParser'; +import EmbeddedCheckoutStylesheet from './EmbeddedCheckoutStylesheet'; export default function createEmbeddedCheckoutStylesheet() { const embeddedCheckoutStyleParser = new EmbeddedCheckoutStyleParser(); diff --git a/packages/core/src/app/embeddedCheckout/createEmbeddedCheckoutSupport.spec.ts b/packages/core/src/app/embeddedCheckout/createEmbeddedCheckoutSupport.spec.ts index 3e1ce70f85..d7e0253696 100644 --- a/packages/core/src/app/embeddedCheckout/createEmbeddedCheckoutSupport.spec.ts +++ b/packages/core/src/app/embeddedCheckout/createEmbeddedCheckoutSupport.spec.ts @@ -3,23 +3,23 @@ import { createLanguageService } from '@bigcommerce/checkout-sdk'; import { NoopCheckoutSupport } from '../checkout'; import createEmbeddedCheckoutSupport from './createEmbeddedCheckoutSupport'; -import * as isEmbeddedModule from './isEmbedded'; import EmbeddedCheckoutSupport from './EmbeddedCheckoutSupport'; +import * as isEmbeddedModule from './isEmbedded'; describe('createEmbeddedCheckoutSupport()', () => { it('returns embedded checkout support if in embedded mode', () => { - jest.spyOn(isEmbeddedModule, 'default') - .mockReturnValue(true); + jest.spyOn(isEmbeddedModule, 'default').mockReturnValue(true); - expect(createEmbeddedCheckoutSupport(createLanguageService())) - .toBeInstanceOf(EmbeddedCheckoutSupport); + expect(createEmbeddedCheckoutSupport(createLanguageService())).toBeInstanceOf( + EmbeddedCheckoutSupport, + ); }); it('returns noop checkout support if not in embedded mode', () => { - jest.spyOn(isEmbeddedModule, 'default') - .mockReturnValue(false); + jest.spyOn(isEmbeddedModule, 'default').mockReturnValue(false); - expect(createEmbeddedCheckoutSupport(createLanguageService())) - .toBeInstanceOf(NoopCheckoutSupport); + expect(createEmbeddedCheckoutSupport(createLanguageService())).toBeInstanceOf( + NoopCheckoutSupport, + ); }); }); diff --git a/packages/core/src/app/embeddedCheckout/createEmbeddedCheckoutSupport.ts b/packages/core/src/app/embeddedCheckout/createEmbeddedCheckoutSupport.ts index d56043ebf0..88bc343457 100644 --- a/packages/core/src/app/embeddedCheckout/createEmbeddedCheckoutSupport.ts +++ b/packages/core/src/app/embeddedCheckout/createEmbeddedCheckoutSupport.ts @@ -2,22 +2,13 @@ import { LanguageService } from '@bigcommerce/checkout-sdk'; import { CheckoutSupport, NoopCheckoutSupport } from '../checkout'; -import isEmbedded from './isEmbedded'; import EmbeddedCheckoutSupport from './EmbeddedCheckoutSupport'; +import isEmbedded from './isEmbedded'; -const UNSUPPORTED_METHODS = [ - 'afterpay', - 'amazon', - 'chasepay', - 'googlepay', - 'klarna', - 'masterpass', -]; +const UNSUPPORTED_METHODS = ['afterpay', 'amazon', 'chasepay', 'googlepay', 'klarna', 'masterpass']; -export default function createEmbeddedCheckoutSupport( - language: LanguageService -): CheckoutSupport { - return isEmbedded() ? - new EmbeddedCheckoutSupport(UNSUPPORTED_METHODS, language) : - new NoopCheckoutSupport(); +export default function createEmbeddedCheckoutSupport(language: LanguageService): CheckoutSupport { + return isEmbedded() + ? new EmbeddedCheckoutSupport(UNSUPPORTED_METHODS, language) + : new NoopCheckoutSupport(); } diff --git a/packages/core/src/app/embeddedCheckout/errors/EmbeddedCheckoutUnsupportedError.ts b/packages/core/src/app/embeddedCheckout/errors/EmbeddedCheckoutUnsupportedError.ts index 0060f5ae5d..39645d097b 100644 --- a/packages/core/src/app/embeddedCheckout/errors/EmbeddedCheckoutUnsupportedError.ts +++ b/packages/core/src/app/embeddedCheckout/errors/EmbeddedCheckoutUnsupportedError.ts @@ -1,4 +1,4 @@ -import { setPrototypeOf, CustomError } from '../../common/error'; +import { CustomError, setPrototypeOf } from '../../common/error'; export class EmbeddedCheckoutUnsupportedError extends CustomError { constructor(message: string) { diff --git a/packages/core/src/app/embeddedCheckout/isEmbedded.spec.ts b/packages/core/src/app/embeddedCheckout/isEmbedded.spec.ts index 7b891734b2..f077518c4b 100644 --- a/packages/core/src/app/embeddedCheckout/isEmbedded.spec.ts +++ b/packages/core/src/app/embeddedCheckout/isEmbedded.spec.ts @@ -2,18 +2,14 @@ import isEmbedded from './isEmbedded'; describe('isEmbedded', () => { it('returns true if URL is embedded-checkout', () => { - expect(isEmbedded('/embedded-checkout')) - .toEqual(true); + expect(isEmbedded('/embedded-checkout')).toBe(true); - expect(isEmbedded('/embedded-checkout/order-confirmation')) - .toEqual(true); + expect(isEmbedded('/embedded-checkout/order-confirmation')).toBe(true); }); it('returns false if URL is not embedded-checkout', () => { - expect(isEmbedded('/checkout')) - .toEqual(false); + expect(isEmbedded('/checkout')).toBe(false); - expect(isEmbedded('/checkout/order-confirmation')) - .toEqual(false); + expect(isEmbedded('/checkout/order-confirmation')).toBe(false); }); }); diff --git a/packages/core/src/app/embeddedCheckout/isEmbedded.ts b/packages/core/src/app/embeddedCheckout/isEmbedded.ts index 789baffbb4..ef96a46d93 100644 --- a/packages/core/src/app/embeddedCheckout/isEmbedded.ts +++ b/packages/core/src/app/embeddedCheckout/isEmbedded.ts @@ -1,6 +1,4 @@ -export default function isEmbedded( - pathname: string = document.location.pathname -): boolean { +export default function isEmbedded(pathname: string = document.location.pathname): boolean { const basePath = `/${pathname.split('/')[1]}`; return basePath === '/embedded-checkout'; diff --git a/packages/core/src/app/formFields/getCustomFormFieldsValidationSchema.spec.tsx b/packages/core/src/app/formFields/getCustomFormFieldsValidationSchema.spec.tsx index 3d13e9e2cb..ff1feb3f55 100644 --- a/packages/core/src/app/formFields/getCustomFormFieldsValidationSchema.spec.tsx +++ b/packages/core/src/app/formFields/getCustomFormFieldsValidationSchema.spec.tsx @@ -3,7 +3,10 @@ import { ObjectSchema, ValidationError } from 'yup'; import { getFormFields } from '../address/formField.mock'; import { getShippingAddress } from '../shipping/shipping-addresses.mock'; -import getCustomFormFieldsValidationSchema, { CustomFormFieldValues, TranslateValidationErrorFunction } from './getCustomFormFieldsValidationSchema'; +import getCustomFormFieldsValidationSchema, { + CustomFormFieldValues, + TranslateValidationErrorFunction, +} from './getCustomFormFieldsValidationSchema'; import getFormFieldsValidationSchema, { FormFieldValues } from './getFormFieldsValidationSchema'; describe('getCustomFormFieldsValidationSchema', () => { @@ -25,19 +28,23 @@ describe('getCustomFormFieldsValidationSchema', () => { }); it('throws if invalid characters are present', async () => { - const errors = await schema.validate({ - ...getShippingAddress(), - firstName: 'Luis<>', - }).catch((error: ValidationError) => error.message); - - expect(errors).toEqual('firstName must match the following: "/^[^<>]*$/"'); + const errors = await schema + .validate({ + ...getShippingAddress(), + firstName: 'Luis<>', + }) + .catch((error: ValidationError) => error.message); + + expect(errors).toBe('firstName must match the following: "/^[^<>]*$/"'); }); it('does not throw if valid characters are present', async () => { - expect(await schema.isValid({ - ...getShippingAddress(), - firstName: 'Luis{}:;()`/-\'', - })).toBeTruthy(); + expect( + await schema.isValid({ + ...getShippingAddress(), + firstName: "Luis{}:;()`/-'", + }), + ).toBeTruthy(); }); }); @@ -45,51 +52,61 @@ describe('getCustomFormFieldsValidationSchema', () => { let schema: ObjectSchema; beforeEach(() => { - schema = getCustomFormFieldsValidationSchema({ formFields: [ - ...formFields, - { - custom: true, - min: 3, - max: 5, - fieldType: 'text', - id: 'field_100', - name: 'field_100', - required: false, - type: 'integer', - } as any, - ], translate }); + schema = getCustomFormFieldsValidationSchema({ + formFields: [ + ...formFields, + { + custom: true, + min: 3, + max: 5, + fieldType: 'text', + id: 'field_100', + name: 'field_100', + required: false, + type: 'integer', + } as any, + ], + translate, + }); }); it('throws if min validation fails', async () => { - const errors = await schema.validate({ - ...getShippingAddress(), - customFields: { - field_100: 2, - }, - }).catch((error: ValidationError) => error.message); - - expect(errors).toEqual('customFields.field_100 must be greater than or equal to 3'); + const errors = await schema + .validate({ + ...getShippingAddress(), + customFields: { + field_100: 2, + }, + }) + .catch((error: ValidationError) => error.message); + + expect(errors).toBe('customFields.field_100 must be greater than or equal to 3'); }); it('throws if max validation fails', async () => { - const errors = await schema.validate({ - ...getShippingAddress(), - customFields: { - field_100: 6, - }, - }).catch((error: ValidationError) => error.message); - - expect(errors).toEqual('customFields.field_100 must be less than or equal to 5'); + const errors = await schema + .validate({ + ...getShippingAddress(), + customFields: { + field_100: 6, + }, + }) + .catch((error: ValidationError) => error.message); + + expect(errors).toBe('customFields.field_100 must be less than or equal to 5'); }); it('resolves if min/max validation pass', async () => { const spy = jest.fn(); - await schema.validate({ - ...getShippingAddress(), - customFields: { - field_100: 4, - }, - }).then(spy); + + await schema + .validate({ + ...getShippingAddress(), + customFields: { + field_100: 4, + }, + }) + .then(spy); expect(spy).toHaveBeenCalled(); }); @@ -99,39 +116,47 @@ describe('getCustomFormFieldsValidationSchema', () => { let schema: ObjectSchema; beforeEach(() => { - schema = getCustomFormFieldsValidationSchema({ formFields: [ - ...formFields, - { - options: { items: [{ value: 'x' }, { value: 'y' }] }, - fieldType: 'dropdown', - id: 'field_100', - name: 'field_100', - required: true, - type: 'string', - custom: true, - } as any, - ], translate }); + schema = getCustomFormFieldsValidationSchema({ + formFields: [ + ...formFields, + { + options: { items: [{ value: 'x' }, { value: 'y' }] }, + fieldType: 'dropdown', + id: 'field_100', + name: 'field_100', + required: true, + type: 'string', + custom: true, + } as any, + ], + translate, + }); }); it('throws if value empty', async () => { - const errors = await schema.validate({ - ...getShippingAddress(), - customFields: { - field_100: '', - }, - }).catch((error: ValidationError) => error.message); - - expect(errors).toEqual('customFields.field_100 is a required field'); + const errors = await schema + .validate({ + ...getShippingAddress(), + customFields: { + field_100: '', + }, + }) + .catch((error: ValidationError) => error.message); + + expect(errors).toBe('customFields.field_100 is a required field'); }); it('resolves if valid value', async () => { const spy = jest.fn(); - await schema.validate({ - ...getShippingAddress(), - customFields: { - field_100: 'x', - }, - }).then(spy); + + await schema + .validate({ + ...getShippingAddress(), + customFields: { + field_100: 'x', + }, + }) + .then(spy); expect(spy).toHaveBeenCalled(); }); diff --git a/packages/core/src/app/formFields/getCustomFormFieldsValidationSchema.ts b/packages/core/src/app/formFields/getCustomFormFieldsValidationSchema.ts index e675f64d46..53cf3f745c 100644 --- a/packages/core/src/app/formFields/getCustomFormFieldsValidationSchema.ts +++ b/packages/core/src/app/formFields/getCustomFormFieldsValidationSchema.ts @@ -1,18 +1,28 @@ import { FormField } from '@bigcommerce/checkout-sdk'; import { memoize } from '@bigcommerce/memoize'; -import { array, date, number, object, string, ArraySchema, NumberSchema, ObjectSchema, Schema } from 'yup'; +import { + array, + ArraySchema, + date, + number, + NumberSchema, + object, + ObjectSchema, + Schema, + string, +} from 'yup'; import { DynamicFormFieldType } from '../ui/form'; -export type TranslateValidationErrorFunction = (( +export type TranslateValidationErrorFunction = ( validationType: 'max' | 'min' | 'required' | 'invalid', field: { name: string; label: string; min?: number; max?: number; - } -) => string | undefined); + }, +) => string | undefined; export interface FormFieldsValidationSchemaOptions { formFields: FormField[]; @@ -45,13 +55,15 @@ export default memoize(function getCustomFormFieldsValidationSchema({ // validation when it's optional .strict(true) .nullable(true) - .transform((value, originalValue) => originalValue === '' ? null : value); + .transform((value, originalValue) => + originalValue === '' ? null : value, + ); } else if (type === 'integer') { schema[name] = number() // Transform NaN values to undefined to avoid empty string (empty input) to fail number // validation when it's optional .strict(true) - .transform(value => isNaN(value) ? undefined : value); + .transform((value) => (isNaN(value) ? undefined : value)); maxValue = typeof max === 'number' ? max : undefined; minValue = typeof min === 'number' ? min : undefined; @@ -62,29 +74,32 @@ export default memoize(function getCustomFormFieldsValidationSchema({ } if (maxValue !== undefined) { - schema[name] = (schema[name] as NumberSchema).max(maxValue, - translate('max', { label, name, max: maxValue + 1 }) + schema[name] = (schema[name] as NumberSchema).max( + maxValue, + translate('max', { label, name, max: maxValue + 1 }), ); } if (minValue !== undefined) { - schema[name] = (schema[name] as NumberSchema).min(minValue, - translate('min', { label, name, min: minValue - 1 }) + schema[name] = (schema[name] as NumberSchema).min( + minValue, + translate('min', { label, name, min: minValue - 1 }), ); } if (required) { const requiredErrorMessage = translate('required', { name, label }); - schema[name] = fieldType === DynamicFormFieldType.checkbox ? - (schema[name] as ArraySchema).min(1, requiredErrorMessage) : - (schema[name] as ArraySchema).required(requiredErrorMessage); + schema[name] = + fieldType === DynamicFormFieldType.checkbox + ? (schema[name] as ArraySchema).min(1, requiredErrorMessage) + : (schema[name] as ArraySchema).required( + requiredErrorMessage, + ); } return schema; - }, - {} as { [key: string]: Schema } - ) + }, {} as { [key: string]: Schema }), ).nullable(true), }) as ObjectSchema; }); diff --git a/packages/core/src/app/formFields/getFormFieldsValidationSchema.spec.tsx b/packages/core/src/app/formFields/getFormFieldsValidationSchema.spec.tsx index 93bb7bf259..1e042b0099 100644 --- a/packages/core/src/app/formFields/getFormFieldsValidationSchema.spec.tsx +++ b/packages/core/src/app/formFields/getFormFieldsValidationSchema.spec.tsx @@ -4,7 +4,10 @@ import { getFormFields } from '../address/formField.mock'; import { getShippingAddress } from '../shipping/shipping-addresses.mock'; import { TranslateValidationErrorFunction } from './getCustomFormFieldsValidationSchema'; -import { default as getFormFieldsValidationSchema, FormFieldValues } from './getFormFieldsValidationSchema'; +import { + FormFieldValues, + default as getFormFieldsValidationSchema, +} from './getFormFieldsValidationSchema'; describe('getFormFielsValidationSchema', () => { const formFields = getFormFields(); @@ -29,81 +32,93 @@ describe('getFormFielsValidationSchema', () => { translate, }); - const errors = await schema.validate({ - ...getShippingAddress(), - firstName: undefined, - }).catch((error: ValidationError) => error.message); + const errors = await schema + .validate({ + ...getShippingAddress(), + firstName: undefined, + }) + .catch((error: ValidationError) => error.message); - expect(errors).toEqual('firstName is a required field'); + expect(errors).toBe('firstName is a required field'); }); it('throws if missing required field with translated error', async () => { const schema = getFormFieldsValidationSchema({ formFields, translate }); - const errors = await schema.validate({ - ...getShippingAddress(), - firstName: undefined, - }).catch((error: ValidationError) => error.message); + const errors = await schema + .validate({ + ...getShippingAddress(), + firstName: undefined, + }) + .catch((error: ValidationError) => error.message); - expect(translate) - .toHaveBeenCalledWith('required', { - label: 'First Name', - name: 'firstName', - }); + expect(translate).toHaveBeenCalledWith('required', { + label: 'First Name', + name: 'firstName', + }); - expect(errors) - .toEqual('firstName is a required field'); + expect(errors).toBe('firstName is a required field'); }); describe('when custom integer field is present', () => { let schema: ObjectSchema>; beforeEach(() => { - schema = getFormFieldsValidationSchema({ formFields: [ - ...formFields, - { - custom: true, - min: 3, - max: 5, - fieldType: 'text', - id: 'field_100', - name: 'field_100', - required: false, - type: 'integer', - } as any, - ], translate }); + schema = getFormFieldsValidationSchema({ + formFields: [ + ...formFields, + { + custom: true, + min: 3, + max: 5, + fieldType: 'text', + id: 'field_100', + name: 'field_100', + required: false, + type: 'integer', + } as any, + ], + translate, + }); }); it('throws if min validation fails', async () => { - const errors = await schema.validate({ - ...getShippingAddress(), - customFields: { - field_100: 2, - }, - }).catch((error: ValidationError) => error.message); - - expect(errors).toEqual('customFields.field_100 must be greater than or equal to 3'); + const errors = await schema + .validate({ + ...getShippingAddress(), + customFields: { + field_100: 2, + }, + }) + .catch((error: ValidationError) => error.message); + + expect(errors).toBe('customFields.field_100 must be greater than or equal to 3'); }); it('throws if max validation fails', async () => { - const errors = await schema.validate({ - ...getShippingAddress(), - customFields: { - field_100: 6, - }, - }).catch((error: ValidationError) => error.message); - - expect(errors).toEqual('customFields.field_100 must be less than or equal to 5'); + const errors = await schema + .validate({ + ...getShippingAddress(), + customFields: { + field_100: 6, + }, + }) + .catch((error: ValidationError) => error.message); + + expect(errors).toBe('customFields.field_100 must be less than or equal to 5'); }); it('resolves if min/max validation pass', async () => { const spy = jest.fn(); - await schema.validate({ - ...getShippingAddress(), - customFields: { - field_100: 4, - }, - }).then(spy); + + await schema + .validate({ + ...getShippingAddress(), + customFields: { + field_100: 4, + }, + }) + .then(spy); expect(spy).toHaveBeenCalled(); }); diff --git a/packages/core/src/app/formFields/getFormFieldsValidationSchema.ts b/packages/core/src/app/formFields/getFormFieldsValidationSchema.ts index 2d7bb6de36..7bb0d46807 100644 --- a/packages/core/src/app/formFields/getFormFieldsValidationSchema.ts +++ b/packages/core/src/app/formFields/getFormFieldsValidationSchema.ts @@ -1,7 +1,9 @@ import { memoize } from '@bigcommerce/memoize'; -import { object, string, ObjectSchema, StringSchema } from 'yup'; +import { object, ObjectSchema, string, StringSchema } from 'yup'; -import getCustomFormFieldsValidationSchema, { FormFieldsValidationSchemaOptions } from './getCustomFormFieldsValidationSchema'; +import getCustomFormFieldsValidationSchema, { + FormFieldsValidationSchemaOptions, +} from './getCustomFormFieldsValidationSchema'; export const WHITELIST_REGEXP = /^[^<>]*$/; @@ -20,17 +22,19 @@ export default memoize(function getFormFieldsValidationSchema({ schema[name] = string(); if (required) { - schema[name] = schema[name].trim().required(translate('required', { label, name })); + schema[name] = schema[name] + .trim() + .required(translate('required', { label, name })); } schema[name] = schema[name].matches( WHITELIST_REGEXP, - translate('invalid', { name, label }) + translate('invalid', { name, label }), ); return schema; - }, - {} as { [key: string]: StringSchema } - ), - }).concat(getCustomFormFieldsValidationSchema({ formFields, translate })) as ObjectSchema; + }, {} as { [key: string]: StringSchema }), + }).concat( + getCustomFormFieldsValidationSchema({ formFields, translate }), + ) as ObjectSchema; }); diff --git a/packages/core/src/app/formFields/index.ts b/packages/core/src/app/formFields/index.ts index 5f611e2a7f..bc949b5eb1 100644 --- a/packages/core/src/app/formFields/index.ts +++ b/packages/core/src/app/formFields/index.ts @@ -1,5 +1,7 @@ - -export { default as getFormFieldsValidationSchema, FormFieldValues } from './getFormFieldsValidationSchema'; +export { + default as getFormFieldsValidationSchema, + FormFieldValues, +} from './getFormFieldsValidationSchema'; export { default as getCustomFormFieldsValidationSchema, FormFieldsValidationSchemaOptions, diff --git a/packages/core/src/app/formFields/mapCustomFormFieldsFromFormValues.ts b/packages/core/src/app/formFields/mapCustomFormFieldsFromFormValues.ts index 34f913e6e6..ddcd003559 100644 --- a/packages/core/src/app/formFields/mapCustomFormFieldsFromFormValues.ts +++ b/packages/core/src/app/formFields/mapCustomFormFieldsFromFormValues.ts @@ -1,15 +1,17 @@ import { forIn, isDate, padStart } from 'lodash'; -export default function mapCustomFormFieldsFromFormValues( - customFieldsObject: { [id: string]: any } -): Array<{fieldId: string; fieldValue: string}> { - const customFields: Array<{fieldId: string; fieldValue: string}> = []; +export default function mapCustomFormFieldsFromFormValues(customFieldsObject: { + [id: string]: any; +}): Array<{ fieldId: string; fieldValue: string }> { + const customFields: Array<{ fieldId: string; fieldValue: string }> = []; + forIn(customFieldsObject, (value, key) => { let fieldValue: string; if (isDate(value)) { const padMonth = padStart((value.getMonth() + 1).toString(), 2, '0'); - const padDay = padStart((value.getDate()).toString(), 2, '0'); + const padDay = padStart(value.getDate().toString(), 2, '0'); + fieldValue = `${value.getFullYear()}-${padMonth}-${padDay}`; } else { fieldValue = value; diff --git a/packages/core/src/app/geography/countries.mock.ts b/packages/core/src/app/geography/countries.mock.ts index 3b9bb9731d..4d536008d9 100644 --- a/packages/core/src/app/geography/countries.mock.ts +++ b/packages/core/src/app/geography/countries.mock.ts @@ -1,11 +1,7 @@ import { Country } from '@bigcommerce/checkout-sdk'; export function getCountries(): Country[] { - return [ - getAustralia(), - getUnitedStates(), - getJapan(), - ]; + return [getAustralia(), getUnitedStates(), getJapan()]; } export function getAustralia(): Country { diff --git a/packages/core/src/app/giftCertificate/AppliedGiftCertificate.spec.tsx b/packages/core/src/app/giftCertificate/AppliedGiftCertificate.spec.tsx index 04545e9da9..f5e8199e31 100644 --- a/packages/core/src/app/giftCertificate/AppliedGiftCertificate.spec.tsx +++ b/packages/core/src/app/giftCertificate/AppliedGiftCertificate.spec.tsx @@ -1,14 +1,14 @@ import { shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; -import { getGiftCertificate } from './giftCertificate.mock'; import AppliedGiftCertificate from './AppliedGiftCertificate'; +import { getGiftCertificate } from './giftCertificate.mock'; describe('AppliedGiftCertificate', () => { let component: ShallowWrapper; beforeEach(() => { - component = shallow(); + component = shallow(); }); it('renders markup that matches snapshot', () => { diff --git a/packages/core/src/app/giftCertificate/AppliedGiftCertificate.tsx b/packages/core/src/app/giftCertificate/AppliedGiftCertificate.tsx index 9642ee604c..797185516b 100644 --- a/packages/core/src/app/giftCertificate/AppliedGiftCertificate.tsx +++ b/packages/core/src/app/giftCertificate/AppliedGiftCertificate.tsx @@ -1,5 +1,5 @@ import { GiftCertificate } from '@bigcommerce/checkout-sdk'; -import React, { memo, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo } from 'react'; import { ShopperCurrency } from '../currency'; import { TranslatedString } from '../locale'; @@ -8,35 +8,28 @@ export interface AppliedGiftCertificateProps { giftCertificate: GiftCertificate; } -const AppliedGiftCertificate: FunctionComponent = ({ giftCertificate }) => ( -
    +const AppliedGiftCertificate: FunctionComponent = ({ + giftCertificate, +}) => ( +
    - - - - { ' ' } - + + {' '} - { giftCertificate.remaining > 0 && - - - { ' ' } - - - + {giftCertificate.remaining > 0 && ( + + {' '} + + + - } + )} - - { giftCertificate.code } - + {giftCertificate.code}
    ); diff --git a/packages/core/src/app/giftCertificate/isGiftCertificatePayment.ts b/packages/core/src/app/giftCertificate/isGiftCertificatePayment.ts index a6f723c788..e5cec79888 100644 --- a/packages/core/src/app/giftCertificate/isGiftCertificatePayment.ts +++ b/packages/core/src/app/giftCertificate/isGiftCertificatePayment.ts @@ -1,7 +1,11 @@ -import { CheckoutPayment, GiftCertificateOrderPayment, OrderPayment } from '@bigcommerce/checkout-sdk'; +import { + CheckoutPayment, + GiftCertificateOrderPayment, + OrderPayment, +} from '@bigcommerce/checkout-sdk'; export default function isGiftCertificatePayment( - payment: OrderPayment | CheckoutPayment + payment: OrderPayment | CheckoutPayment, ): payment is GiftCertificateOrderPayment { return payment.providerId === 'giftcertificate'; } diff --git a/packages/core/src/app/giftCertificate/mapFromPayments.spec.ts b/packages/core/src/app/giftCertificate/mapFromPayments.spec.ts index df34c6ec30..359804f46c 100644 --- a/packages/core/src/app/giftCertificate/mapFromPayments.spec.ts +++ b/packages/core/src/app/giftCertificate/mapFromPayments.spec.ts @@ -4,15 +4,14 @@ import mapFromPayments from './mapFromPayments'; describe('mapFromPayments()', () => { it('returns mapped gift certificates', () => { - expect(mapFromPayments(getOrder().payments || [])) - .toEqual([ - { - code: 'gc', - remaining: 3, - used: 7, - balance: 10, - purchaseDate: '', - }, - ]); + expect(mapFromPayments(getOrder().payments || [])).toEqual([ + { + code: 'gc', + remaining: 3, + used: 7, + balance: 10, + purchaseDate: '', + }, + ]); }); }); diff --git a/packages/core/src/app/giftCertificate/mapFromPayments.ts b/packages/core/src/app/giftCertificate/mapFromPayments.ts index aae7e6df61..0ef21f932b 100644 --- a/packages/core/src/app/giftCertificate/mapFromPayments.ts +++ b/packages/core/src/app/giftCertificate/mapFromPayments.ts @@ -3,13 +3,11 @@ import { GiftCertificate, OrderPayments } from '@bigcommerce/checkout-sdk'; import isGiftCertificatePayment from './isGiftCertificatePayment'; export default function mapFromPayments(payments: OrderPayments): GiftCertificate[] { - return payments - .filter(isGiftCertificatePayment) - .map(({ amount, detail }) => ({ - code: detail.code, - remaining: detail.remaining, - used: amount, - balance: amount + detail.remaining, - purchaseDate: '', - })); + return payments.filter(isGiftCertificatePayment).map(({ amount, detail }) => ({ + code: detail.code, + remaining: detail.remaining, + used: amount, + balance: amount + detail.remaining, + purchaseDate: '', + })); } diff --git a/packages/core/src/app/guestSignup/AccountService.spec.ts b/packages/core/src/app/guestSignup/AccountService.spec.ts index 586ccb83a4..43b006cb09 100644 --- a/packages/core/src/app/guestSignup/AccountService.spec.ts +++ b/packages/core/src/app/guestSignup/AccountService.spec.ts @@ -46,8 +46,9 @@ describe('AccountService', () => { }); it('calls internal api', () => { - expect(requestSender.put) - .toBeCalledWith('/internalapi/v1/checkout/customer', { body: payload }); + expect(requestSender.put).toHaveBeenCalledWith('/internalapi/v1/checkout/customer', { + body: payload, + }); }); it('returns customer', () => { diff --git a/packages/core/src/app/guestSignup/AccountService.ts b/packages/core/src/app/guestSignup/AccountService.ts index d7ce7ce075..97276d73ee 100644 --- a/packages/core/src/app/guestSignup/AccountService.ts +++ b/packages/core/src/app/guestSignup/AccountService.ts @@ -10,13 +10,11 @@ export interface CustomerCreateRequestBody { } export default class AccountService { - constructor( - private requestSender: RequestSender = createRequestSender() - ) { } + constructor(private requestSender: RequestSender = createRequestSender()) {} create(body: CustomerCreateRequestBody): Promise { return this.requestSender .put('/internalapi/v1/checkout/customer', { body }) - .then(response => response.body.data.customer); + .then((response) => response.body.data.customer); } } diff --git a/packages/core/src/app/guestSignup/GuestSignUpForm.spec.tsx b/packages/core/src/app/guestSignup/GuestSignUpForm.spec.tsx index d2a1b63c48..243316b87d 100644 --- a/packages/core/src/app/guestSignup/GuestSignUpForm.spec.tsx +++ b/packages/core/src/app/guestSignup/GuestSignUpForm.spec.tsx @@ -1,4 +1,4 @@ -import { mount, render, ReactWrapper } from 'enzyme'; +import { mount, ReactWrapper, render } from 'enzyme'; import { noop } from 'lodash'; import React from 'react'; @@ -22,107 +22,109 @@ describe('GuestSignUpForm', () => { localeContext = createLocaleContext(getStoreConfig()); component = mount( - + - + , ); }); it('matches snapshot', () => { const shallowComponent = render( - + - + , ); - expect(shallowComponent) - .toMatchSnapshot(); + expect(shallowComponent).toMatchSnapshot(); }); it('matches snapshot when customer cannot be created', () => { const shallowComponent = render( - + - + , ); - expect(shallowComponent) - .toMatchSnapshot(); + expect(shallowComponent).toMatchSnapshot(); }); it('notifies when user clicks on "create account" button', async () => { - component.find('input[name="password"]') + component + .find('input[name="password"]') .simulate('change', { target: { value: 'password1', name: 'password' } }); - component.find('input[name="confirmPassword"]') + component + .find('input[name="confirmPassword"]') .simulate('change', { target: { value: 'password1', name: 'confirmPassword' } }); - component.find('form') - .simulate('submit'); + component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(handleSignUp) - .toHaveBeenCalled(); + expect(handleSignUp).toHaveBeenCalled(); }); it('displays error message if password is not valid', async () => { - component.find('input[name="password"]') + component + .find('input[name="password"]') .simulate('change', { target: { value: '1', name: 'password' } }); - component.find('form') - .simulate('submit'); + component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(component.find('[data-test="password-field-error-message"]').text()) - .toEqual(passwordRequirements.description); + expect(component.find('[data-test="password-field-error-message"]').text()).toEqual( + passwordRequirements.description, + ); }); it('displays error message if passwords are missing', async () => { - component.find('form') - .simulate('submit'); + component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(component.find('[data-test="password-field-error-message"]').text()) - .toEqual(passwordRequirements.description); + expect(component.find('[data-test="password-field-error-message"]').text()).toEqual( + passwordRequirements.description, + ); - expect(component.find('[data-test="confirm-password-field-error-message"]').text()) - .toEqual('This field is required'); + expect(component.find('[data-test="confirm-password-field-error-message"]').text()).toBe( + 'This field is required', + ); }); it('displays error message if passwords do not match', async () => { - component.find('input[name="password"]') + component + .find('input[name="password"]') .simulate('change', { target: { value: 'password1', name: 'password' } }); - component.find('input[name="confirmPassword"]') + component + .find('input[name="confirmPassword"]') .simulate('change', { target: { value: 'password2', name: 'confirmPassword' } }); - component.find('form') - .simulate('submit'); + component.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(component.find('[data-test="confirm-password-field-error-message"]').text()) - .toEqual('Passwords do not match'); + expect(component.find('[data-test="confirm-password-field-error-message"]').text()).toBe( + 'Passwords do not match', + ); }); }); diff --git a/packages/core/src/app/guestSignup/GuestSignUpForm.tsx b/packages/core/src/app/guestSignup/GuestSignUpForm.tsx index 091d0c683b..89de4c0379 100644 --- a/packages/core/src/app/guestSignup/GuestSignUpForm.tsx +++ b/packages/core/src/app/guestSignup/GuestSignUpForm.tsx @@ -1,9 +1,9 @@ -import { withFormik, FormikProps } from 'formik'; -import React, { memo, FunctionComponent } from 'react'; +import { FormikProps, withFormik } from 'formik'; +import React, { FunctionComponent, memo } from 'react'; import { object, ref, string } from 'yup'; import { PasswordRequirements } from '../customer'; -import { withLanguage, TranslatedString, WithLanguageProps } from '../locale'; +import { TranslatedString, withLanguage, WithLanguageProps } from '../locale'; import { Button, ButtonVariant } from '../ui/button'; import { Fieldset, Form, Legend } from '../ui/form'; @@ -21,60 +21,89 @@ export interface SignUpFormValues { confirmPassword: string; } -const GuestSignUpForm: FunctionComponent> = ({ - isSigningUp, - customerCanBeCreated, - passwordRequirements: { minLength }, -}) => ( +const GuestSignUpForm: FunctionComponent< + SignUpFormProps & WithLanguageProps & FormikProps +> = ({ isSigningUp, customerCanBeCreated, passwordRequirements: { minLength } }) => (
    - + } > - { !customerCanBeCreated && + {!customerCanBeCreated && (

    -

    } +

    + )}
    - +
    ); -export default withLanguage(withFormik({ - mapPropsToValues: () => ({ - password: '', - confirmPassword: '', - }), - handleSubmit: (values, { props: { onSignUp } }) => { - onSignUp(values); - }, - validationSchema: ({ - language, - passwordRequirements: { description, numeric, alpha, minLength }, - }: SignUpFormProps & WithLanguageProps) => object({ - password: string() - .required(description || language.translate('customer.password_required_error')) - .matches(numeric, description || language.translate('customer.password_number_required_error')) - .matches(alpha, description || language.translate('customer.password_letter_required_error')) - .min(minLength, description || language.translate('customer.password_under_minimum_length_error')) - .max(100, language.translate('customer.password_over_maximum_length_error')), - confirmPassword: string() - .required(language.translate('customer.password_confirmation_required_error')) - .oneOf([ref('password')], language.translate('customer.password_confirmation_error')), - }), -})(memo(GuestSignUpForm))); +export default withLanguage( + withFormik({ + mapPropsToValues: () => ({ + password: '', + confirmPassword: '', + }), + handleSubmit: (values, { props: { onSignUp } }) => { + onSignUp(values); + }, + validationSchema: ({ + language, + passwordRequirements: { description, numeric, alpha, minLength }, + }: SignUpFormProps & WithLanguageProps) => + object({ + password: string() + .required(description || language.translate('customer.password_required_error')) + .matches( + numeric, + description || + language.translate('customer.password_number_required_error'), + ) + .matches( + alpha, + description || + language.translate('customer.password_letter_required_error'), + ) + .min( + minLength, + description || + language.translate('customer.password_under_minimum_length_error'), + ) + .max(100, language.translate('customer.password_over_maximum_length_error')), + confirmPassword: string() + .required(language.translate('customer.password_confirmation_required_error')) + .oneOf( + [ref('password')], + language.translate('customer.password_confirmation_error'), + ), + }), + })(memo(GuestSignUpForm)), +); diff --git a/packages/core/src/app/guestSignup/PasswordSavedSuccessAlert.tsx b/packages/core/src/app/guestSignup/PasswordSavedSuccessAlert.tsx index 10be12d890..d183a45895 100644 --- a/packages/core/src/app/guestSignup/PasswordSavedSuccessAlert.tsx +++ b/packages/core/src/app/guestSignup/PasswordSavedSuccessAlert.tsx @@ -4,7 +4,7 @@ import { TranslatedString } from '../locale'; import { Alert, AlertType } from '../ui/alert'; const PasswordSavedSuccessAlert: FunctionComponent = () => ( - + diff --git a/packages/core/src/app/guestSignup/SignUpPasswordField.tsx b/packages/core/src/app/guestSignup/SignUpPasswordField.tsx index 94ff541add..e5ac427dd8 100644 --- a/packages/core/src/app/guestSignup/SignUpPasswordField.tsx +++ b/packages/core/src/app/guestSignup/SignUpPasswordField.tsx @@ -1,5 +1,5 @@ import { FieldProps } from 'formik'; -import React, { memo, useCallback, useMemo, Fragment, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo, useCallback, useMemo } from 'react'; import { TranslatedString } from '../locale'; import { FormField, TextInput } from '../ui/form'; @@ -8,53 +8,50 @@ export interface PasswordField { minLength: number; } -const SignUpPasswordField: FunctionComponent = ({ - minLength, -}) => { - const renderPasswordInput = useCallback(({ field }: FieldProps) => ( - - ), []); - - const renderPasswordConfirmationInput = useCallback(({ field }: FieldProps) => ( - - ), []); - - const passwordLabelContent = useMemo(() => ( - - - { ' ' } - - { `${minLength}-` } - - - - ), [minLength]); - - const passwordConfirmationLabelContent = useMemo(() => ( - - ), []); - - return - - - - ; +const SignUpPasswordField: FunctionComponent = ({ minLength }) => { + const renderPasswordInput = useCallback( + ({ field }: FieldProps) => , + [], + ); + + const renderPasswordConfirmationInput = useCallback( + ({ field }: FieldProps) => , + [], + ); + + const passwordLabelContent = useMemo( + () => ( + <> + {' '} + + {`${minLength}-`} + + + + ), + [minLength], + ); + + const passwordConfirmationLabelContent = useMemo( + () => , + [], + ); + + return ( + <> + + + + + ); }; export default memo(SignUpPasswordField); diff --git a/packages/core/src/app/guestSignup/SignedUpSuccessAlert.tsx b/packages/core/src/app/guestSignup/SignedUpSuccessAlert.tsx index 001735eafe..ee5b51d6a6 100644 --- a/packages/core/src/app/guestSignup/SignedUpSuccessAlert.tsx +++ b/packages/core/src/app/guestSignup/SignedUpSuccessAlert.tsx @@ -4,7 +4,7 @@ import { TranslatedString } from '../locale'; import { Alert, AlertType } from '../ui/alert'; const SignedUpSuccessAlert: FunctionComponent = () => ( - + diff --git a/packages/core/src/app/guestSignup/errors/AccountCreationFailedError.ts b/packages/core/src/app/guestSignup/errors/AccountCreationFailedError.ts index 3fad642941..3002b60ceb 100644 --- a/packages/core/src/app/guestSignup/errors/AccountCreationFailedError.ts +++ b/packages/core/src/app/guestSignup/errors/AccountCreationFailedError.ts @@ -1,4 +1,4 @@ -import { setPrototypeOf, CustomError } from '../../common/error'; +import { CustomError, setPrototypeOf } from '../../common/error'; import { getLanguageService } from '../../locale'; export default class AccountCreationFailedError extends CustomError { diff --git a/packages/core/src/app/guestSignup/errors/AccountCreationRequirementsError.ts b/packages/core/src/app/guestSignup/errors/AccountCreationRequirementsError.ts index 5030c6b972..be64e6cd9d 100644 --- a/packages/core/src/app/guestSignup/errors/AccountCreationRequirementsError.ts +++ b/packages/core/src/app/guestSignup/errors/AccountCreationRequirementsError.ts @@ -1,4 +1,4 @@ -import { setPrototypeOf, CustomError } from '../../common/error'; +import { CustomError, setPrototypeOf } from '../../common/error'; import { getLanguageService } from '../../locale'; export default class AccountCreationRequirementsError extends CustomError { @@ -6,7 +6,9 @@ export default class AccountCreationRequirementsError extends CustomError { super({ name: 'ACCOUNT_CREATION_REQUIREMENTS_ERROR', message: requirements, - title: getLanguageService().translate('customer.create_account_requirements_error_heading'), + title: getLanguageService().translate( + 'customer.create_account_requirements_error_heading', + ), data, }); diff --git a/packages/core/src/app/loader.spec.ts b/packages/core/src/app/loader.spec.ts index 64bb45dec2..97d13babe3 100644 --- a/packages/core/src/app/loader.spec.ts +++ b/packages/core/src/app/loader.spec.ts @@ -1,8 +1,8 @@ import { getScriptLoader, getStylesheetLoader } from '@bigcommerce/script-loader'; import { noop } from 'lodash'; -import { loadFiles, AssetManifest, LoadFilesOptions } from './loader'; import AppExport from './AppExport'; +import { AssetManifest, loadFiles, LoadFilesOptions } from './loader'; jest.mock('@bigcommerce/script-loader', () => { return { @@ -29,24 +29,12 @@ describe('loadFiles', () => { manifestJson = { appVersion: '1.0.0', - css: [ - 'vendor.css', - 'main.css', - ], + css: ['vendor.css', 'main.css'], dynamicChunks: { - css: [ - 'step-a.css', - 'step-b.css', - ], - js: [ - 'step-a.js', - 'step-b.js', - ], + css: ['step-a.css', 'step-b.css'], + js: ['step-a.js', 'step-b.js'], }, - js: [ - 'vendor.js', - 'main.js', - ], + js: ['vendor.js', 'main.js'], }; (global as any).MANIFEST_JSON = manifestJson; @@ -67,48 +55,43 @@ describe('loadFiles', () => { it('loads required JS files listed in manifest', async () => { await loadFiles(options); - expect(getScriptLoader().loadScripts) - .toHaveBeenCalledWith([ - 'https://cdn.foo.bar/vendor.js', - 'https://cdn.foo.bar/main.js', - ]); + expect(getScriptLoader().loadScripts).toHaveBeenCalledWith([ + 'https://cdn.foo.bar/vendor.js', + 'https://cdn.foo.bar/main.js', + ]); }); it('loads required CSS files listed in manifest', async () => { await loadFiles(options); - expect(getStylesheetLoader().loadStylesheets) - .toHaveBeenCalledWith([ - 'https://cdn.foo.bar/vendor.css', - 'https://cdn.foo.bar/main.css', - ], { prepend: true }); + expect(getStylesheetLoader().loadStylesheets).toHaveBeenCalledWith( + ['https://cdn.foo.bar/vendor.css', 'https://cdn.foo.bar/main.css'], + { prepend: true }, + ); }); it('prefetches dynamic JS chunks listed in manifest', async () => { await loadFiles(options); - expect(getScriptLoader().preloadScripts) - .toHaveBeenCalledWith([ - 'https://cdn.foo.bar/step-a.js', - 'https://cdn.foo.bar/step-b.js', - ], { prefetch: true }); + expect(getScriptLoader().preloadScripts).toHaveBeenCalledWith( + ['https://cdn.foo.bar/step-a.js', 'https://cdn.foo.bar/step-b.js'], + { prefetch: true }, + ); }); it('prefetches dynamic CSS chunks listed in manifest', async () => { await loadFiles(options); - expect(getStylesheetLoader().preloadStylesheets) - .toHaveBeenCalledWith([ - 'https://cdn.foo.bar/step-a.css', - 'https://cdn.foo.bar/step-b.css', - ], { prefetch: true }); + expect(getStylesheetLoader().preloadStylesheets).toHaveBeenCalledWith( + ['https://cdn.foo.bar/step-a.css', 'https://cdn.foo.bar/step-b.css'], + { prefetch: true }, + ); }); it('resolves with app version', async () => { const result = await loadFiles(options); - expect(result.appVersion) - .toEqual(manifestJson.appVersion); + expect(result.appVersion).toEqual(manifestJson.appVersion); }); it('resolves with render checkout function', async () => { @@ -119,12 +102,11 @@ describe('loadFiles', () => { containerId: 'checkout-app', }); - expect((global as any).checkout.renderCheckout) - .toHaveBeenCalledWith({ - checkoutId: 'abc', - containerId: 'checkout-app', - publicPath: options.publicPath, - }); + expect((global as any).checkout.renderCheckout).toHaveBeenCalledWith({ + checkoutId: 'abc', + containerId: 'checkout-app', + publicPath: options.publicPath, + }); }); it('resolves with render order confirmation function', async () => { @@ -135,35 +117,31 @@ describe('loadFiles', () => { containerId: 'checkout-app', }); - expect((global as any).checkout.renderOrderConfirmation) - .toHaveBeenCalledWith({ - orderId: 123, - containerId: 'checkout-app', - publicPath: options.publicPath, - }); + expect((global as any).checkout.renderOrderConfirmation).toHaveBeenCalledWith({ + orderId: 123, + containerId: 'checkout-app', + publicPath: options.publicPath, + }); }); it('does not wait for prefetched scripts to resolve', async () => { const scriptLoader = getScriptLoader(); - jest.spyOn(scriptLoader, 'preloadScripts') - .mockImplementation((_url, opt) => { - return (opt && opt.prefetch ? new Promise(noop) : Promise.resolve()); - }); + jest.spyOn(scriptLoader, 'preloadScripts').mockImplementation((_url, opt) => { + return opt && opt.prefetch ? new Promise(noop) : Promise.resolve(); + }); - expect(await loadFiles(options)) - .toBeDefined(); + expect(await loadFiles(options)).toBeDefined(); }); it('initializes language service with default translations', async () => { await loadFiles(options); - expect(appExports.initializeLanguageService) - .toHaveBeenCalledWith({ - defaultTranslations: expect.any(Object), - locale: expect.any(String), - locales: expect.any(Object), - translations: expect.any(Object), - }); + expect(appExports.initializeLanguageService).toHaveBeenCalledWith({ + defaultTranslations: expect.any(Object), + locale: expect.any(String), + locales: expect.any(Object), + translations: expect.any(Object), + }); }); }); diff --git a/packages/core/src/app/loader.ts b/packages/core/src/app/loader.ts index b8f5d7ee6d..96c83d6d60 100644 --- a/packages/core/src/app/loader.ts +++ b/packages/core/src/app/loader.ts @@ -1,11 +1,11 @@ import { getScriptLoader, getStylesheetLoader } from '@bigcommerce/script-loader'; +import { isAppExport } from './AppExport'; import { RenderCheckoutOptions } from './checkout'; import { configurePublicPath } from './common/bundler'; import { isRecordContainingKey, joinPaths } from './common/utility'; import { getDefaultTranslations, isLanguageWindow } from './locale'; import { RenderOrderConfirmationOptions } from './order'; -import { isAppExport } from './AppExport'; declare const LIBRARY_NAME: string; declare const MANIFEST_JSON: AssetManifest; @@ -32,42 +32,33 @@ export function loadFiles(options?: LoadFilesOptions): Promise const { appVersion, css = [], - dynamicChunks: { - css: cssDynamicChunks = [], - js: jsDynamicChunks = [], - }, + dynamicChunks: { css: cssDynamicChunks = [], js: jsDynamicChunks = [] }, js = [], } = MANIFEST_JSON; - const scripts = getScriptLoader().loadScripts( - js.map(path => joinPaths(publicPath, path)) - ); + const scripts = getScriptLoader().loadScripts(js.map((path) => joinPaths(publicPath, path))); const stylesheets = getStylesheetLoader().loadStylesheets( - css.map(path => joinPaths(publicPath, path)), - { prepend: true } + css.map((path) => joinPaths(publicPath, path)), + { prepend: true }, ); getScriptLoader().preloadScripts( - jsDynamicChunks.map(path => joinPaths(publicPath, path)), - { prefetch: true } + jsDynamicChunks.map((path) => joinPaths(publicPath, path)), + { prefetch: true }, ); getStylesheetLoader().preloadStylesheets( - cssDynamicChunks.map(path => joinPaths(publicPath, path)), - { prefetch: true } + cssDynamicChunks.map((path) => joinPaths(publicPath, path)), + { prefetch: true }, ); - const languageConfig = isLanguageWindow(window) ? - window.language : - { locale: 'en', locales: {}, translations: {} }; + const languageConfig = isLanguageWindow(window) + ? window.language + : { locale: 'en', locales: {}, translations: {} }; - return Promise.all([ - getDefaultTranslations(languageConfig.locale), - scripts, - stylesheets, - ]) - .then(([defaultTranslations]) => { + return Promise.all([getDefaultTranslations(languageConfig.locale), scripts, stylesheets]).then( + ([defaultTranslations]) => { if (!isRecordContainingKey(window, LIBRARY_NAME)) { throw new Error(`'${LIBRARY_NAME}' property is not available in window.`); } @@ -75,14 +66,13 @@ export function loadFiles(options?: LoadFilesOptions): Promise const appExport = window[LIBRARY_NAME]; if (!isAppExport(appExport)) { - throw new Error('The functions required to bootstrap the application are not available.'); + throw new Error( + 'The functions required to bootstrap the application are not available.', + ); } - const { - renderCheckout, - renderOrderConfirmation, - initializeLanguageService, - } = appExport; + const { renderCheckout, renderOrderConfirmation, initializeLanguageService } = + appExport; initializeLanguageService({ ...languageConfig, @@ -91,8 +81,10 @@ export function loadFiles(options?: LoadFilesOptions): Promise return { appVersion, - renderCheckout: renderOptions => renderCheckout({ publicPath, ...renderOptions }), - renderOrderConfirmation: renderOptions => renderOrderConfirmation({ publicPath, ...renderOptions }), + renderCheckout: (renderOptions) => renderCheckout({ publicPath, ...renderOptions }), + renderOrderConfirmation: (renderOptions) => + renderOrderConfirmation({ publicPath, ...renderOptions }), }; - }); + }, + ); } diff --git a/packages/core/src/app/locale/LocaleProvider.spec.tsx b/packages/core/src/app/locale/LocaleProvider.spec.tsx index 078dbd6f7d..275bc93c78 100644 --- a/packages/core/src/app/locale/LocaleProvider.spec.tsx +++ b/packages/core/src/app/locale/LocaleProvider.spec.tsx @@ -1,4 +1,4 @@ -import { createCheckoutService, CheckoutService } from '@bigcommerce/checkout-sdk'; +import { CheckoutService, createCheckoutService } from '@bigcommerce/checkout-sdk'; import { mount } from 'enzyme'; import React, { FunctionComponent } from 'react'; @@ -13,50 +13,42 @@ describe('LocaleProvider', () => { beforeEach(() => { checkoutService = createCheckoutService(); - jest.spyOn(checkoutService.getState().data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutService.getState().data, 'getConfig').mockReturnValue(getStoreConfig()); }); it('provides locale context to child components', () => { const Child: FunctionComponent = jest.fn(() => null); const component = mount( - + - { props => props && } + {(props) => props && } - + , ); - expect(component.find(Child).prop('currency')) - .toBeDefined(); + expect(component.find(Child).prop('currency')).toBeDefined(); - expect(component.find(Child).prop('language')) - .toBeDefined(); + expect(component.find(Child).prop('language')).toBeDefined(); - expect(component.find(Child).prop('date')) - .toBeDefined(); + expect(component.find(Child).prop('date')).toBeDefined(); }); it('provides locale context without currency service and date to child components when config is not available yet', () => { - jest.spyOn(checkoutService.getState().data, 'getConfig') - .mockReturnValue(undefined); + jest.spyOn(checkoutService.getState().data, 'getConfig').mockReturnValue(undefined); const Child: FunctionComponent = jest.fn(() => null); const component = mount( - + - { props => props && } + {(props) => props && } - + , ); - expect(component.find(Child).prop('currency')) - .not.toBeDefined(); + expect(component.find(Child).prop('currency')).toBeUndefined(); - expect(component.find(Child).prop('language')) - .toBeDefined(); + expect(component.find(Child).prop('language')).toBeDefined(); - expect(component.find(Child).prop('date')) - .not.toBeDefined(); + expect(component.find(Child).prop('date')).toBeUndefined(); }); }); diff --git a/packages/core/src/app/locale/LocaleProvider.tsx b/packages/core/src/app/locale/LocaleProvider.tsx index 474e6e1078..05f1e47cc3 100644 --- a/packages/core/src/app/locale/LocaleProvider.tsx +++ b/packages/core/src/app/locale/LocaleProvider.tsx @@ -1,4 +1,4 @@ -import { createCurrencyService, CheckoutService, StoreConfig } from '@bigcommerce/checkout-sdk'; +import { CheckoutService, createCurrencyService, StoreConfig } from '@bigcommerce/checkout-sdk'; import { memoizeOne } from '@bigcommerce/memoize'; import React, { Component, ReactNode } from 'react'; @@ -20,12 +20,13 @@ class LocaleProvider extends Component { private unsubscribe?: () => void; private getContextValue = memoizeOne((config?: StoreConfig) => { - return { currency: config ? createCurrencyService(config) : undefined, - date: config ? { - inputFormat: config.inputDateFormat, - } : undefined, + date: config + ? { + inputFormat: config.inputDateFormat, + } + : undefined, language: this.languageService, }; }); @@ -37,7 +38,7 @@ class LocaleProvider extends Component { ({ data }) => { this.setState({ config: data.getConfig() }); }, - ({ data }) => data.getConfig() + ({ data }) => data.getConfig(), ); } @@ -53,8 +54,8 @@ class LocaleProvider extends Component { const { config } = this.state; return ( - - { children } + + {children} ); } diff --git a/packages/core/src/app/locale/TranslatedHtml.spec.tsx b/packages/core/src/app/locale/TranslatedHtml.spec.tsx index 7c237cd175..2606eaab4c 100644 --- a/packages/core/src/app/locale/TranslatedHtml.spec.tsx +++ b/packages/core/src/app/locale/TranslatedHtml.spec.tsx @@ -27,34 +27,33 @@ describe('TranslatedHtml', () => { it('renders translated Html', () => { const component = mount( - + - + , ); - expect(component.html()) - .toEqual('abc'); + expect(component.html()).toBe('abc'); }); it('sanitizes translated Html', () => { const component = mount( - + - + , ); - expect(component.html()) - .toEqual(''); + expect(component.html()).toBe(''); }); it('allows anchor tags with target attribute', () => { const component = mount( - + - + , ); - expect(component.html()) - .toEqual('Link'); + expect(component.html()).toBe( + 'Link', + ); }); }); diff --git a/packages/core/src/app/locale/TranslatedHtml.tsx b/packages/core/src/app/locale/TranslatedHtml.tsx index 7c5730426f..c59de71de2 100644 --- a/packages/core/src/app/locale/TranslatedHtml.tsx +++ b/packages/core/src/app/locale/TranslatedHtml.tsx @@ -13,9 +13,10 @@ const TranslatedHtml: FunctionComponent id, language, }) => ( - ); diff --git a/packages/core/src/app/locale/TranslatedLink.spec.tsx b/packages/core/src/app/locale/TranslatedLink.spec.tsx index 9bb0437814..4f19b3a688 100644 --- a/packages/core/src/app/locale/TranslatedLink.spec.tsx +++ b/packages/core/src/app/locale/TranslatedLink.spec.tsx @@ -2,8 +2,8 @@ import { mount, render } from 'enzyme'; import { noop } from 'lodash'; import React from 'react'; -import { getLocaleContext } from './localeContext.mock'; import LocaleContext from './LocaleContext'; +import { getLocaleContext } from './localeContext.mock'; import TranslatedLink from './TranslatedLink'; describe('TranslatedLink Component', () => { @@ -12,41 +12,47 @@ describe('TranslatedLink Component', () => { jest.spyOn(localeContext.language, 'translate'); it('renders translated link', () => { - expect(render( - - - )) - .toMatchSnapshot(); + expect( + render( + + + , + ), + ).toMatchSnapshot(); }); it('renders translated text if theres no link', () => { - expect(mount( - - - ).html()) - .toEqual('Create Account'); + expect( + mount( + + + , + ).html(), + ).toBe('Create Account'); }); it('calls onClick when link is clicked', () => { const onClick = jest.fn(); mount( - + - - ).find('[data-test="link"]').simulate('click'); + , + ) + .find('[data-test="link"]') + .simulate('click'); expect(onClick).toHaveBeenCalled(); }); diff --git a/packages/core/src/app/locale/TranslatedLink.tsx b/packages/core/src/app/locale/TranslatedLink.tsx index 2e09682b4e..f95f4b7b37 100644 --- a/packages/core/src/app/locale/TranslatedLink.tsx +++ b/packages/core/src/app/locale/TranslatedLink.tsx @@ -3,8 +3,8 @@ import React, { FunctionComponent, MouseEventHandler } from 'react'; import { preventDefault } from '../common/dom'; import { parseAnchor } from '../common/utility'; -import withLanguage, { WithLanguageProps } from './withLanguage'; import { TranslatedStringProps } from './TranslatedString'; +import withLanguage, { WithLanguageProps } from './withLanguage'; export type TranslatedLinkProps = TranslatedStringProps & { testId?: string; @@ -21,19 +21,17 @@ const TranslatedLink: FunctionComponent const translatedString = language.translate(id, data); const parsedString = parseAnchor(translatedString); - return parsedString.length ? + return parsedString.length ? ( <> - { parsedString[0] } - - { parsedString[1] } + {parsedString[0]} + + {parsedString[1]} - { parsedString[2] } - : - <>{ translatedString }; + {parsedString[2]} + + ) : ( + <>{translatedString} + ); }; export default withLanguage(TranslatedLink); diff --git a/packages/core/src/app/locale/TranslatedString.spec.tsx b/packages/core/src/app/locale/TranslatedString.spec.tsx index a2f6c40d75..765b2d55dd 100644 --- a/packages/core/src/app/locale/TranslatedString.spec.tsx +++ b/packages/core/src/app/locale/TranslatedString.spec.tsx @@ -1,8 +1,8 @@ import React from 'react'; import testRenderer from 'react-test-renderer'; -import { getLocaleContext } from './localeContext.mock'; import LocaleContext from './LocaleContext'; +import { getLocaleContext } from './localeContext.mock'; import TranslatedString from './TranslatedString'; describe('TranslatedString Component', () => { @@ -11,33 +11,50 @@ describe('TranslatedString Component', () => { jest.spyOn(localeContext.language, 'translate'); it('renders translated string', () => { - expect(testRenderer.create( - - - ) - .toJSON()) - .toMatchSnapshot(); + expect( + testRenderer + .create( + + + , + ) + .toJSON(), + ).toMatchSnapshot(); }); it('calls language.translate', () => { - expect(testRenderer.create( - - - ) - .toJSON()); - - expect(localeContext.language.translate).toHaveBeenCalledWith('address.address_line_1_label', undefined); + expect( + testRenderer + .create( + + + , + ) + .toJSON(), + ); + + expect(localeContext.language.translate).toHaveBeenCalledWith( + 'address.address_line_1_label', + undefined, + ); }); it('calls language.translate with data when passed', () => { const data = { foo: 'xyz' }; - expect(testRenderer.create( - - - ) - .toJSON()); - - expect(localeContext.language.translate).toHaveBeenCalledWith('address.address_line_1_label', data); + expect( + testRenderer + .create( + + + , + ) + .toJSON(), + ); + + expect(localeContext.language.translate).toHaveBeenCalledWith( + 'address.address_line_1_label', + data, + ); }); }); diff --git a/packages/core/src/app/locale/TranslatedString.tsx b/packages/core/src/app/locale/TranslatedString.tsx index b5e5f4b112..9099cfc508 100644 --- a/packages/core/src/app/locale/TranslatedString.tsx +++ b/packages/core/src/app/locale/TranslatedString.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, FunctionComponent } from 'react'; +import React, { FunctionComponent } from 'react'; import withLanguage, { WithLanguageProps } from './withLanguage'; @@ -11,10 +11,6 @@ const TranslatedString: FunctionComponent ( - - { language.translate(id, data) } - -); +}) => <>{language.translate(id, data)}; export default withLanguage(TranslatedString); diff --git a/packages/core/src/app/locale/getDefaultTranslations.spec.ts b/packages/core/src/app/locale/getDefaultTranslations.spec.ts index da6b8198d8..4355c66483 100644 --- a/packages/core/src/app/locale/getDefaultTranslations.spec.ts +++ b/packages/core/src/app/locale/getDefaultTranslations.spec.ts @@ -2,80 +2,56 @@ import getDefaultTranslations from './getDefaultTranslations'; describe('getDefaultTranslations', () => { it('returns Danish translations when da locale is specified', async () => { - expect(await getDefaultTranslations('da')) - // eslint-disable-next-line import/no-internal-modules - .toEqual(require('./translations/da.json')); + expect(await getDefaultTranslations('da')).toEqual(require('./translations/da.json')); }); it('returns French translations when fr locale is specified', async () => { - expect(await getDefaultTranslations('fr')) - // eslint-disable-next-line import/no-internal-modules - .toEqual(require('./translations/fr.json')); + expect(await getDefaultTranslations('fr')).toEqual(require('./translations/fr.json')); }); it('returns Canadian French translations when fr-CA locale is specified', async () => { - expect(await getDefaultTranslations('fr-CA')) - // eslint-disable-next-line import/no-internal-modules - .toEqual(require('./translations/fr.json')); + expect(await getDefaultTranslations('fr-CA')).toEqual(require('./translations/fr.json')); }); it('returns Portuguese translations when pt locale is specified', async () => { - expect(await getDefaultTranslations('pt')) - // eslint-disable-next-line import/no-internal-modules - .toEqual(require('./translations/pt.json')); + expect(await getDefaultTranslations('pt')).toEqual(require('./translations/pt.json')); }); it('returns Brazilian Portuguese translations when pt-BR locale is specified', async () => { - expect(await getDefaultTranslations('pt-BR')) - // eslint-disable-next-line import/no-internal-modules - .toEqual(require('./translations/pt-BR.json')); + expect(await getDefaultTranslations('pt-BR')).toEqual(require('./translations/pt-BR.json')); }); it('returns Spanish translations when es locale is specified', async () => { - expect(await getDefaultTranslations('es')) - // eslint-disable-next-line import/no-internal-modules - .toEqual(require('./translations/es.json')); + expect(await getDefaultTranslations('es')).toEqual(require('./translations/es.json')); }); it('returns Castilian Spanish translations when es-mx locale is specified', async () => { - expect(await getDefaultTranslations('es-MX')) - // eslint-disable-next-line import/no-internal-modules - .toEqual(require('./translations/es-MX.json')); + expect(await getDefaultTranslations('es-MX')).toEqual(require('./translations/es-MX.json')); }); it('returns Latin American Spanish translations when es-419 locale is specified', async () => { - expect(await getDefaultTranslations('es-419')) - // eslint-disable-next-line import/no-internal-modules - .toEqual(require('./translations/es-419.json')); + expect(await getDefaultTranslations('es-419')).toEqual( + require('./translations/es-419.json'), + ); }); it('returns Argentine Spanish translations when es-AR locale is specified', async () => { - expect(await getDefaultTranslations('es-AR')) - // eslint-disable-next-line import/no-internal-modules - .toEqual(require('./translations/es-AR.json')); + expect(await getDefaultTranslations('es-AR')).toEqual(require('./translations/es-AR.json')); }); it('returns Chilean Spanish translations when es-CL locale is specified', async () => { - expect(await getDefaultTranslations('es-CL')) - // eslint-disable-next-line import/no-internal-modules - .toEqual(require('./translations/es-CL.json')); + expect(await getDefaultTranslations('es-CL')).toEqual(require('./translations/es-CL.json')); }); it('returns Colombian Spanish translations when es-CO locale is specified', async () => { - expect(await getDefaultTranslations('es-CO')) - // eslint-disable-next-line import/no-internal-modules - .toEqual(require('./translations/es-CO.json')); + expect(await getDefaultTranslations('es-CO')).toEqual(require('./translations/es-CO.json')); }); it('returns Peruvian Spanish translations when es-PE locale is specified', async () => { - expect(await getDefaultTranslations('es-PE')) - // eslint-disable-next-line import/no-internal-modules - .toEqual(require('./translations/es-PE.json')); + expect(await getDefaultTranslations('es-PE')).toEqual(require('./translations/es-PE.json')); }); it('returns Norwegian translations when no locale is specified', async () => { - expect(await getDefaultTranslations('no')) - // eslint-disable-next-line import/no-internal-modules - .toEqual(require('./translations/no.json')); + expect(await getDefaultTranslations('no')).toEqual(require('./translations/no.json')); }); }); diff --git a/packages/core/src/app/locale/getDefaultTranslations.ts b/packages/core/src/app/locale/getDefaultTranslations.ts index 80ef669cad..4f7699606a 100644 --- a/packages/core/src/app/locale/getDefaultTranslations.ts +++ b/packages/core/src/app/locale/getDefaultTranslations.ts @@ -3,75 +3,95 @@ import { Translations } from '@bigcommerce/checkout-sdk'; import { FALLBACK_TRANSLATIONS } from './translations'; const AVAILABLE_TRANSLATIONS: Record Promise<{ default: unknown }>> = { - es: () => import( - /* webpackChunkName: "translations-es" */ - './translations/es.json' - ), - 'es-419': () => import( - /* webpackChunkName: "translations-es-419" */ - './translations/es-419.json' - ), - 'es-AR': () => import( - /* webpackChunkName: "translations-es-ar" */ - './translations/es-AR.json' - ), - 'es-CL': () => import( - /* webpackChunkName: "translations-es-cl" */ - './translations/es-CL.json' - ), - 'es-CO': () => import( - /* webpackChunkName: "translations-es-co" */ - './translations/es-CO.json' - ), - 'es-MX': () => import( - /* webpackChunkName: "translations-es-mx" */ - './translations/es-MX.json' - ), - 'es-PE': () => import( - /* webpackChunkName: "translations-es-pe" */ - './translations/es-PE.json' - ), - da: () => import( - /* webpackChunkName: "translations-da" */ - './translations/da.json' - ), - de: () => import( - /* webpackChunkName: "translations-de" */ - './translations/de.json' - ), - fr: () => import( - /* webpackChunkName: "translations-fr" */ - './translations/fr.json' - ), - it: () => import( - /* webpackChunkName: "translations-it" */ - './translations/it.json' - ), - nl: () => import( - /* webpackChunkName: "translations-nl" */ - './translations/nl.json' - ), - no: () => import( - /* webpackChunkName: "translations-no" */ - './translations/no.json' - ), - 'pt-BR': () => import( - /* webpackChunkName: "translations-pt-br" */ - './translations/pt-BR.json' - ), - pt: () => import( - /* webpackChunkName: "translations-pt" */ - './translations/pt.json' - ), - sv: () => import( - /* webpackChunkName: "translations-sv" */ - './translations/sv.json' - ), - en: () => Promise.resolve(({ default: FALLBACK_TRANSLATIONS })), + es: () => + import( + /* webpackChunkName: "translations-es" */ + './translations/es.json' + ), + 'es-419': () => + import( + /* webpackChunkName: "translations-es-419" */ + './translations/es-419.json' + ), + 'es-AR': () => + import( + /* webpackChunkName: "translations-es-ar" */ + './translations/es-AR.json' + ), + 'es-CL': () => + import( + /* webpackChunkName: "translations-es-cl" */ + './translations/es-CL.json' + ), + 'es-CO': () => + import( + /* webpackChunkName: "translations-es-co" */ + './translations/es-CO.json' + ), + 'es-MX': () => + import( + /* webpackChunkName: "translations-es-mx" */ + './translations/es-MX.json' + ), + 'es-PE': () => + import( + /* webpackChunkName: "translations-es-pe" */ + './translations/es-PE.json' + ), + da: () => + import( + /* webpackChunkName: "translations-da" */ + './translations/da.json' + ), + de: () => + import( + /* webpackChunkName: "translations-de" */ + './translations/de.json' + ), + fr: () => + import( + /* webpackChunkName: "translations-fr" */ + './translations/fr.json' + ), + it: () => + import( + /* webpackChunkName: "translations-it" */ + './translations/it.json' + ), + nl: () => + import( + /* webpackChunkName: "translations-nl" */ + './translations/nl.json' + ), + no: () => + import( + /* webpackChunkName: "translations-no" */ + './translations/no.json' + ), + 'pt-BR': () => + import( + /* webpackChunkName: "translations-pt-br" */ + './translations/pt-BR.json' + ), + pt: () => + import( + /* webpackChunkName: "translations-pt" */ + './translations/pt.json' + ), + sv: () => + import( + /* webpackChunkName: "translations-sv" */ + './translations/sv.json' + ), + en: () => Promise.resolve({ default: FALLBACK_TRANSLATIONS }), }; -export default async function getDefaultTranslations(requestedLocale: string): Promise { - const loadTranslations = AVAILABLE_TRANSLATIONS[requestedLocale] ?? AVAILABLE_TRANSLATIONS[requestedLocale.split('-')[0]]; +export default async function getDefaultTranslations( + requestedLocale: string, +): Promise { + const loadTranslations = + AVAILABLE_TRANSLATIONS[requestedLocale] ?? + AVAILABLE_TRANSLATIONS[requestedLocale.split('-')[0]]; return loadTranslations ? asTranslations((await loadTranslations()).default) : {}; } diff --git a/packages/core/src/app/locale/getLanguageService.spec.ts b/packages/core/src/app/locale/getLanguageService.spec.ts index 01ec3a0509..36b09c9a77 100644 --- a/packages/core/src/app/locale/getLanguageService.spec.ts +++ b/packages/core/src/app/locale/getLanguageService.spec.ts @@ -19,28 +19,22 @@ describe('getLanguageService', () => { }); it('returns fallback language service if not initialized', () => { - expect(getLanguageService()) - .toBeDefined(); - expect(getLanguageService().translate('greeting')) - .toEqual('optimized_checkout.greeting'); - expect(getLanguageService().translate('address.address_line_1_label')) - .toEqual('Address'); + expect(getLanguageService()).toBeDefined(); + expect(getLanguageService().translate('greeting')).toBe('optimized_checkout.greeting'); + expect(getLanguageService().translate('address.address_line_1_label')).toBe('Address'); }); it('returns language service if initialized', () => { initializeLanguageService(languageConfig); - expect(getLanguageService()) - .toBeDefined(); - expect(getLanguageService().translate('greeting')) - .toEqual('Hello'); + expect(getLanguageService()).toBeDefined(); + expect(getLanguageService().translate('greeting')).toBe('Hello'); }); it('returns language service with fallback translations', () => { initializeLanguageService(languageConfig); - expect(getLanguageService().translate('address.address_line_1_label')) - .toEqual('Address'); + expect(getLanguageService().translate('address.address_line_1_label')).toBe('Address'); }); it('returns language service with override translations', () => { @@ -55,7 +49,6 @@ describe('getLanguageService', () => { }, }); - expect(getLanguageService().translate('greeting')) - .toEqual('Bonjour'); + expect(getLanguageService().translate('greeting')).toBe('Bonjour'); }); }); diff --git a/packages/core/src/app/locale/getLanguageService.ts b/packages/core/src/app/locale/getLanguageService.ts index 26ebffabc1..95fdcc3641 100644 --- a/packages/core/src/app/locale/getLanguageService.ts +++ b/packages/core/src/app/locale/getLanguageService.ts @@ -5,10 +5,12 @@ import { FALLBACK_LOCALE, FALLBACK_TRANSLATIONS } from './translations'; let languageService: LanguageService | undefined; export default function getLanguageService(): LanguageService { - languageService = languageService ?? createLanguageService({ - fallbackLocale: FALLBACK_LOCALE, - fallbackTranslations: FALLBACK_TRANSLATIONS, - }); + languageService = + languageService ?? + createLanguageService({ + fallbackLocale: FALLBACK_LOCALE, + fallbackTranslations: FALLBACK_TRANSLATIONS, + }); return languageService; } diff --git a/packages/core/src/app/locale/masterpassFormatLocale.spec.ts b/packages/core/src/app/locale/masterpassFormatLocale.spec.ts index 0053abc026..de0cd91a3a 100644 --- a/packages/core/src/app/locale/masterpassFormatLocale.spec.ts +++ b/packages/core/src/app/locale/masterpassFormatLocale.spec.ts @@ -2,22 +2,18 @@ import masterpassFormatLocale from './masterpassFormatLocale'; describe('formatLocale', () => { it('fixes the format of locales with a dash', () => { - expect(masterpassFormatLocale('en-us')) - .toBe('en_us'); + expect(masterpassFormatLocale('en-us')).toBe('en_us'); }); it('fixes the format of locales in uppercase', () => { - expect(masterpassFormatLocale('FR')) - .toBe('fr'); + expect(masterpassFormatLocale('FR')).toBe('fr'); }); it('maintains the value of valid locale', () => { - expect(masterpassFormatLocale('uk_ua')) - .toBe('uk_ua'); + expect(masterpassFormatLocale('uk_ua')).toBe('uk_ua'); }); it('maintains the value of invalid locale', () => { - expect(masterpassFormatLocale('invalid_data')) - .toBe('invalid_data'); + expect(masterpassFormatLocale('invalid_data')).toBe('invalid_data'); }); }); diff --git a/packages/core/src/app/locale/withCurrency.spec.tsx b/packages/core/src/app/locale/withCurrency.spec.tsx index 4c473c8672..2df14a8bd5 100644 --- a/packages/core/src/app/locale/withCurrency.spec.tsx +++ b/packages/core/src/app/locale/withCurrency.spec.tsx @@ -4,8 +4,8 @@ import React from 'react'; import { getStoreConfig } from '../config/config.mock'; import createLocaleContext from './createLocaleContext'; -import withCurrency from './withCurrency'; import LocaleContext, { LocaleContextType } from './LocaleContext'; +import withCurrency from './withCurrency'; describe('withCurrency()', () => { let contextValue: LocaleContextType; @@ -18,12 +18,11 @@ describe('withCurrency()', () => { const Inner = () =>
    ; const Outer = withCurrency(Inner); const container = mount( - + - + , ); - expect(container.find(Inner).prop('currency')) - .toEqual(contextValue.currency); + expect(container.find(Inner).prop('currency')).toEqual(contextValue.currency); }); }); diff --git a/packages/core/src/app/locale/withDate.spec.tsx b/packages/core/src/app/locale/withDate.spec.tsx index 796b012d3f..f41e24f93a 100644 --- a/packages/core/src/app/locale/withDate.spec.tsx +++ b/packages/core/src/app/locale/withDate.spec.tsx @@ -4,8 +4,8 @@ import React from 'react'; import { getStoreConfig } from '../config/config.mock'; import createLocaleContext from './createLocaleContext'; -import withDate from './withDate'; import LocaleContext, { LocaleContextType } from './LocaleContext'; +import withDate from './withDate'; describe('withDate()', () => { let contextValue: Required; @@ -18,12 +18,11 @@ describe('withDate()', () => { const Inner = () =>
    ; const Outer = withDate(Inner); const container = mount( - + - + , ); - expect(container.find(Inner).prop('date')) - .toEqual(contextValue.date); + expect(container.find(Inner).prop('date')).toEqual(contextValue.date); }); }); diff --git a/packages/core/src/app/locale/withLanguage.spec.tsx b/packages/core/src/app/locale/withLanguage.spec.tsx index 8b101b6d55..e30286a94e 100644 --- a/packages/core/src/app/locale/withLanguage.spec.tsx +++ b/packages/core/src/app/locale/withLanguage.spec.tsx @@ -4,8 +4,8 @@ import React from 'react'; import { getStoreConfig } from '../config/config.mock'; import createLocaleContext from './createLocaleContext'; -import withLanguage from './withLanguage'; import LocaleContext, { LocaleContextType } from './LocaleContext'; +import withLanguage from './withLanguage'; describe('withLanguage()', () => { let contextValue: LocaleContextType; @@ -18,12 +18,11 @@ describe('withLanguage()', () => { const Inner = () =>
    ; const Outer = withLanguage(Inner); const container = mount( - + - + , ); - expect(container.find(Inner).prop('language')) - .toEqual(contextValue.language); + expect(container.find(Inner).prop('language')).toEqual(contextValue.language); }); }); diff --git a/packages/core/src/app/order/OrderConfirmation.spec.tsx b/packages/core/src/app/order/OrderConfirmation.spec.tsx index 4e7d3014e8..4fcc4b93b0 100644 --- a/packages/core/src/app/order/OrderConfirmation.spec.tsx +++ b/packages/core/src/app/order/OrderConfirmation.spec.tsx @@ -1,4 +1,12 @@ -import { createCheckoutService, createEmbeddedCheckoutMessenger, CheckoutSelectors, CheckoutService, EmbeddedCheckoutMessenger, StepTracker, BodlService } from '@bigcommerce/checkout-sdk'; +import { + BodlService, + CheckoutSelectors, + CheckoutService, + createCheckoutService, + createEmbeddedCheckoutMessenger, + EmbeddedCheckoutMessenger, + StepTracker, +} from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import React, { FunctionComponent } from 'react'; import { act } from 'react-dom/test-utils'; @@ -10,8 +18,8 @@ import { createEmbeddedCheckoutStylesheet } from '../embeddedCheckout'; import { CreatedCustomer, GuestSignUpForm } from '../guestSignup'; import { LoadingSpinner } from '../ui/loading'; -import { getOrder } from './orders.mock'; import OrderConfirmation, { OrderConfirmationProps } from './OrderConfirmation'; +import { getOrder } from './orders.mock'; import OrderStatus from './OrderStatus'; import OrderSummary from './OrderSummary'; import ThankYouHeader from './ThankYouHeader'; @@ -35,19 +43,17 @@ describe('OrderConfirmation', () => { bodlService = { orderPurchased: jest.fn(), } as unknown as BodlService; - embeddedMessengerMock = createEmbeddedCheckoutMessenger({ parentOrigin: getStoreConfig().links.siteLink }); + embeddedMessengerMock = createEmbeddedCheckoutMessenger({ + parentOrigin: getStoreConfig().links.siteLink, + }); - jest.spyOn(checkoutService, 'loadOrder') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'loadOrder').mockResolvedValue(checkoutState); - jest.spyOn(checkoutState.statuses, 'isLoadingOrder') - .mockReturnValue(true); + jest.spyOn(checkoutState.statuses, 'isLoadingOrder').mockReturnValue(true); - jest.spyOn(checkoutState.data, 'getOrder') - .mockReturnValue(getOrder()); + jest.spyOn(checkoutState.data, 'getOrder').mockReturnValue(getOrder()); - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); defaultProps = { containerId: 'app', @@ -60,123 +66,116 @@ describe('OrderConfirmation', () => { orderId: 105, }; - ComponentTest = props => ( - - + ComponentTest = (props) => ( + + ); }); it('calls trackOrderComplete when config is ready', async () => { - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(undefined); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(undefined); - const component = mount(); + const component = mount(); component.setProps({ config: {} }); component.update(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(stepTracker.trackOrderComplete) - .toHaveBeenCalled(); + expect(stepTracker.trackOrderComplete).toHaveBeenCalled(); }); it('calls bodl event order complete triggered', async () => { - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(undefined); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(undefined); + + const component = mount(); - const component = mount(); component.update(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(bodlService.orderPurchased) - .toHaveBeenCalled(); + expect(bodlService.orderPurchased).toHaveBeenCalled(); }); it('loads passed order ID', () => { - orderConfirmation = mount(); + orderConfirmation = mount(); expect(orderConfirmation.find(LoadingSpinner).prop('isLoading')).toBeTruthy(); expect(checkoutService.loadOrder).toHaveBeenCalledWith(105); }); it('posts message to parent of embedded checkout when order is loaded', async () => { - jest.spyOn(embeddedMessengerMock, 'postFrameLoaded') - .mockImplementation(); + jest.spyOn(embeddedMessengerMock, 'postFrameLoaded').mockImplementation(); - mount(); + mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(embeddedMessengerMock.postFrameLoaded) - .toHaveBeenCalledWith({ contentId: defaultProps.containerId }); + expect(embeddedMessengerMock.postFrameLoaded).toHaveBeenCalledWith({ + contentId: defaultProps.containerId, + }); }); it('attaches additional styles for embedded checkout', async () => { const styles = { text: { color: '#000' } }; - jest.spyOn(embeddedMessengerMock, 'receiveStyles') - .mockImplementation(fn => fn(styles)); + jest.spyOn(embeddedMessengerMock, 'receiveStyles').mockImplementation((fn) => fn(styles)); - jest.spyOn(defaultProps.embeddedStylesheet, 'append') - .mockImplementation(); + jest.spyOn(defaultProps.embeddedStylesheet, 'append').mockImplementation(); - mount(); + mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(defaultProps.embeddedStylesheet.append) - .toHaveBeenCalledWith(styles); + expect(defaultProps.embeddedStylesheet.append).toHaveBeenCalledWith(styles); }); it('renders confirmation page once loading is finished', async () => { - jest.spyOn(checkoutState.statuses, 'isLoadingOrder') - .mockReturnValue(false); + jest.spyOn(checkoutState.statuses, 'isLoadingOrder').mockReturnValue(false); - orderConfirmation = mount(); + orderConfirmation = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - act(() => { orderConfirmation.update(); }); - act(() => { orderConfirmation.update(); }); + act(() => { + orderConfirmation.update(); + }); + act(() => { + orderConfirmation.update(); + }); - expect(orderConfirmation.find(LoadingSpinner).length).toEqual(0); - expect(orderConfirmation.find('.orderConfirmation').length).toEqual(1); - expect(orderConfirmation.find(OrderStatus).length).toEqual(1); - expect(orderConfirmation.find(ThankYouHeader).length).toEqual(1); - expect(orderConfirmation.find(OrderSummary).length).toEqual(1); - expect(orderConfirmation.find(GuestSignUpForm).prop('customerCanBeCreated')) - .toBeTruthy(); + expect(orderConfirmation.find(LoadingSpinner)).toHaveLength(0); + expect(orderConfirmation.find('.orderConfirmation')).toHaveLength(1); + expect(orderConfirmation.find(OrderStatus)).toHaveLength(1); + expect(orderConfirmation.find(ThankYouHeader)).toHaveLength(1); + expect(orderConfirmation.find(OrderSummary)).toHaveLength(1); + expect(orderConfirmation.find(GuestSignUpForm).prop('customerCanBeCreated')).toBeTruthy(); expect(orderConfirmation.find('[data-test="payment-instructions"]')).toMatchSnapshot(); }); it('renders set password form', async () => { - jest.spyOn(checkoutState.statuses, 'isLoadingOrder') - .mockReturnValue(false); + jest.spyOn(checkoutState.statuses, 'isLoadingOrder').mockReturnValue(false); - jest.spyOn(checkoutState.data, 'getOrder') - .mockReturnValue({ - ...getOrder(), - customerId: 1, - }); + jest.spyOn(checkoutState.data, 'getOrder').mockReturnValue({ + ...getOrder(), + customerId: 1, + }); - orderConfirmation = mount(); + orderConfirmation = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(orderConfirmation.find(GuestSignUpForm).prop('customerCanBeCreated')) - .toBeFalsy(); + expect(orderConfirmation.find(GuestSignUpForm).prop('customerCanBeCreated')).toBeFalsy(); }); it('renders continue shopping button', () => { - jest.spyOn(checkoutState.statuses, 'isLoadingOrder') - .mockReturnValue(false); + jest.spyOn(checkoutState.statuses, 'isLoadingOrder').mockReturnValue(false); - orderConfirmation = mount(); + orderConfirmation = mount(); - expect(orderConfirmation.find('.continueButtonContainer form').prop('action')) - .toEqual(getStoreConfig().links.siteLink); + expect(orderConfirmation.find('.continueButtonContainer form').prop('action')).toEqual( + getStoreConfig().links.siteLink, + ); }); }); diff --git a/packages/core/src/app/order/OrderConfirmation.tsx b/packages/core/src/app/order/OrderConfirmation.tsx index fd25fde114..3b921b67af 100644 --- a/packages/core/src/app/order/OrderConfirmation.tsx +++ b/packages/core/src/app/order/OrderConfirmation.tsx @@ -1,15 +1,33 @@ -import { CheckoutSelectors, EmbeddedCheckoutMessenger, EmbeddedCheckoutMessengerOptions, Order, ShopperConfig, StepTracker, BodlService, StoreConfig } from '@bigcommerce/checkout-sdk'; +import { + BodlService, + CheckoutSelectors, + EmbeddedCheckoutMessenger, + EmbeddedCheckoutMessengerOptions, + Order, + ShopperConfig, + StepTracker, + StoreConfig, +} from '@bigcommerce/checkout-sdk'; import classNames from 'classnames'; import DOMPurify from 'dompurify'; -import React, { lazy, Component, Fragment, ReactNode } from 'react'; +import React, { Component, lazy, ReactNode } from 'react'; -import { withCheckout, CheckoutContextProps } from '../checkout'; +import { CheckoutContextProps, withCheckout } from '../checkout'; import { ErrorLogger, ErrorModal } from '../common/error'; import { retry } from '../common/utility'; import { getPasswordRequirementsFromConfig } from '../customer'; -import { isEmbedded, EmbeddedCheckoutStylesheet } from '../embeddedCheckout'; -import { CreatedCustomer, GuestSignUpForm, PasswordSavedSuccessAlert, SignedUpSuccessAlert, SignUpFormValues } from '../guestSignup'; -import { AccountCreationFailedError, AccountCreationRequirementsError } from '../guestSignup/errors'; +import { EmbeddedCheckoutStylesheet, isEmbedded } from '../embeddedCheckout'; +import { + CreatedCustomer, + GuestSignUpForm, + PasswordSavedSuccessAlert, + SignedUpSuccessAlert, + SignUpFormValues, +} from '../guestSignup'; +import { + AccountCreationFailedError, + AccountCreationRequirementsError, +} from '../guestSignup/errors'; import { TranslatedString } from '../locale'; import { Button, ButtonVariant } from '../ui/button'; import { LazyContainer, LoadingSpinner } from '../ui/loading'; @@ -22,15 +40,25 @@ import OrderStatus from './OrderStatus'; import PrintLink from './PrintLink'; import ThankYouHeader from './ThankYouHeader'; -const OrderSummary = lazy(() => retry(() => import( - /* webpackChunkName: "order-summary" */ - './OrderSummary' -))); - -const OrderSummaryDrawer = lazy(() => retry(() => import( - /* webpackChunkName: "order-summary-drawer" */ - './OrderSummaryDrawer' -))); +const OrderSummary = lazy(() => + retry( + () => + import( + /* webpackChunkName: "order-summary" */ + './OrderSummary' + ), + ), +); + +const OrderSummaryDrawer = lazy(() => + retry( + () => + import( + /* webpackChunkName: "order-summary-drawer" */ + './OrderSummaryDrawer' + ), + ), +); export interface OrderConfirmationState { error?: Error; @@ -82,7 +110,7 @@ class OrderConfirmation extends Component< this.embeddedMessenger = messenger; - messenger.receiveStyles(styles => embeddedStylesheet.append(styles)); + messenger.receiveStyles((styles) => embeddedStylesheet.append(styles)); messenger.postFrameLoaded({ contentId: containerId }); createStepTracker().trackOrderComplete(); @@ -92,62 +120,55 @@ class OrderConfirmation extends Component< } render(): ReactNode { - const { - order, - config, - isLoadingOrder, - } = this.props; + const { order, config, isLoadingOrder } = this.props; if (!order || !config || isLoadingOrder()) { - return ; + return ; } const paymentInstructions = getPaymentInstructions(order); const { - storeProfile: { - orderEmail, - storePhoneNumber, - }, + storeProfile: { orderEmail, storePhoneNumber }, shopperConfig, - links: { - siteLink, - }, + links: { siteLink }, } = config; return ( -
    - + - { paymentInstructions && -
    - } - - { this.renderGuestSignUp({ + {paymentInstructions && ( + +
    + + )} + + {this.renderGuestSignUp({ shouldShowPasswordForm: order.customerCanBeCreated, customerCanBeCreated: !order.customerId, shopperConfig, - }) } + })}
    -
    -
    @@ -155,82 +176,88 @@ class OrderConfirmation extends Component<
    - { this.renderOrderSummary() } - { this.renderErrorModal() } + {this.renderOrderSummary()} + {this.renderErrorModal()}
    ); } - private renderGuestSignUp({ customerCanBeCreated, shouldShowPasswordForm, shopperConfig }: { + private renderGuestSignUp({ + customerCanBeCreated, + shouldShowPasswordForm, + shopperConfig, + }: { customerCanBeCreated: boolean; shouldShowPasswordForm: boolean; shopperConfig: ShopperConfig; }): ReactNode { - const { - isSigningUp, - hasSignedUp, - } = this.state; + const { isSigningUp, hasSignedUp } = this.state; const { order } = this.props; - return - { shouldShowPasswordForm && !hasSignedUp && } - - { hasSignedUp && (order?.customerId ? : ) } - ; + return ( + <> + {shouldShowPasswordForm && !hasSignedUp && ( + + )} + + {hasSignedUp && + (order?.customerId ? : )} + + ); } private renderOrderSummary(): ReactNode { - const { - order, - config, - } = this.props; + const { order, config } = this.props; if (!order || !config) { return null; } - const { - currency, - shopperCurrency, - } = config; + const { currency, shopperCurrency } = config; - return <> + return ( - { matched => { + {(matched) => { if (matched) { - return - } - lineItems={ order.lineItems } - shopperCurrency={ shopperCurrency } - storeCurrency={ currency } - total={ order.orderAmount } - /> - ; + return ( + + + } + lineItems={order.lineItems} + shopperCurrency={shopperCurrency} + storeCurrency={currency} + total={order.orderAmount} + /> + + ); } - return ; - } } + return ( + + ); + }} - ; + ); } private renderErrorModal(): ReactNode { @@ -238,9 +265,9 @@ class OrderConfirmation extends Component< return ( ); } @@ -253,9 +280,11 @@ class OrderConfirmation extends Component< const { createAccount, config } = this.props; const shopperConfig = config && config.shopperConfig; - const passwordRequirements = (shopperConfig && - shopperConfig.passwordRequirements && - shopperConfig.passwordRequirements.error) || ''; + const passwordRequirements = + (shopperConfig && + shopperConfig.passwordRequirements && + shopperConfig.passwordRequirements.error) || + ''; this.setState({ isSigningUp: true, @@ -271,18 +300,19 @@ class OrderConfirmation extends Component< isSigningUp: false, }); }) - .catch(error => { + .catch((error) => { this.setState({ - error: (error.status < 500) ? - new AccountCreationRequirementsError(error, passwordRequirements) : - new AccountCreationFailedError(error), + error: + error.status < 500 + ? new AccountCreationRequirementsError(error, passwordRequirements) + : new AccountCreationFailedError(error), hasSignedUp: false, isSigningUp: false, }); }); }; - private handleUnhandledError: (error: Error) => void = error => { + private handleUnhandledError: (error: Error) => void = (error) => { const { errorLogger } = this.props; this.setState({ error }); @@ -295,17 +325,12 @@ class OrderConfirmation extends Component< } export function mapToOrderConfirmationProps( - context: CheckoutContextProps + context: CheckoutContextProps, ): WithCheckoutOrderConfirmationProps | null { const { checkoutState: { - data: { - getOrder, - getConfig, - }, - statuses: { - isLoadingOrder, - }, + data: { getOrder, getConfig }, + statuses: { isLoadingOrder }, }, checkoutService, } = context; diff --git a/packages/core/src/app/order/OrderConfirmationApp.spec.tsx b/packages/core/src/app/order/OrderConfirmationApp.spec.tsx index 05c66aa658..fd503534b8 100644 --- a/packages/core/src/app/order/OrderConfirmationApp.spec.tsx +++ b/packages/core/src/app/order/OrderConfirmationApp.spec.tsx @@ -24,7 +24,7 @@ describe('OrderConfirmationApp', () => { }); it('renders app without crashing', () => { - orderConfirmationApp = shallow(); + orderConfirmationApp = shallow(); expect(orderConfirmationApp).toBeTruthy(); }); diff --git a/packages/core/src/app/order/OrderConfirmationApp.tsx b/packages/core/src/app/order/OrderConfirmationApp.tsx index 5f7482d4c3..569ca4f1c0 100644 --- a/packages/core/src/app/order/OrderConfirmationApp.tsx +++ b/packages/core/src/app/order/OrderConfirmationApp.tsx @@ -1,4 +1,11 @@ -import { createCheckoutService, createEmbeddedCheckoutMessenger, createStepTracker, StepTracker, createBodlService, BodlService } from '@bigcommerce/checkout-sdk'; +import { + BodlService, + createBodlService, + createCheckoutService, + createEmbeddedCheckoutMessenger, + createStepTracker, + StepTracker, +} from '@bigcommerce/checkout-sdk'; import { BrowserOptions } from '@sentry/browser'; import React, { Component, ReactNode } from 'react'; import ReactModal from 'react-modal'; @@ -36,7 +43,7 @@ class OrderConfirmationApp extends Component { { errorTypes: ['UnrecoverableError'], publicPath: props.publicPath, - } + }, ); } @@ -48,17 +55,17 @@ class OrderConfirmationApp extends Component { render(): ReactNode { return ( - - - + + + diff --git a/packages/core/src/app/order/OrderConfirmationSection.tsx b/packages/core/src/app/order/OrderConfirmationSection.tsx index 2b59771756..993bc16ff2 100644 --- a/packages/core/src/app/order/OrderConfirmationSection.tsx +++ b/packages/core/src/app/order/OrderConfirmationSection.tsx @@ -1,11 +1,7 @@ import React, { FunctionComponent } from 'react'; -const OrderConfirmationSection: FunctionComponent = ({ - children, -}) => ( -
    - { children } -
    +const OrderConfirmationSection: FunctionComponent = ({ children }) => ( +
    {children}
    ); export default OrderConfirmationSection; diff --git a/packages/core/src/app/order/OrderStatus.spec.tsx b/packages/core/src/app/order/OrderStatus.spec.tsx index 0574587ce5..0d31fdf2eb 100644 --- a/packages/core/src/app/order/OrderStatus.spec.tsx +++ b/packages/core/src/app/order/OrderStatus.spec.tsx @@ -1,4 +1,4 @@ -import { createCheckoutService, CheckoutService, Order } from '@bigcommerce/checkout-sdk'; +import { CheckoutService, createCheckoutService, Order } from '@bigcommerce/checkout-sdk'; import { mount } from 'enzyme'; import React, { FunctionComponent } from 'react'; @@ -22,9 +22,9 @@ describe('OrderStatus', () => { supportEmail: 'test@example.com', }; - OrderStatusTest = props => ( - - + OrderStatusTest = (props) => ( + + ); }); @@ -32,44 +32,37 @@ describe('OrderStatus', () => { describe('when order is complete', () => { it('renders confirmation message with contact email and phone number if both are provided', () => { const orderStatus = mount( - + , ); - const translationProps = orderStatus.find('[data-test="order-confirmation-order-status-text"]') + const translationProps = orderStatus + .find('[data-test="order-confirmation-order-status-text"]') .find(TranslatedHtml) .props(); - expect(translationProps) - .toEqual({ - data: { - orderNumber: defaultProps.order.orderId, - supportEmail: defaultProps.supportEmail, - supportPhoneNumber: '990', - }, - id: 'order_confirmation.order_with_support_number_text', - }); + expect(translationProps).toEqual({ + data: { + orderNumber: defaultProps.order.orderId, + supportEmail: defaultProps.supportEmail, + supportPhoneNumber: '990', + }, + id: 'order_confirmation.order_with_support_number_text', + }); }); it('renders confirmation message without contact phone number if it is not provided', () => { - const orderStatus = mount( - - ); - const translationProps = orderStatus.find('[data-test="order-confirmation-order-status-text"]') + const orderStatus = mount(); + const translationProps = orderStatus + .find('[data-test="order-confirmation-order-status-text"]') .find(TranslatedHtml) .props(); - expect(translationProps) - .toEqual({ - data: { - orderNumber: defaultProps.order.orderId, - supportEmail: defaultProps.supportEmail, - }, - id: 'order_confirmation.order_without_support_number_text', - }); + expect(translationProps).toEqual({ + data: { + orderNumber: defaultProps.order.orderId, + supportEmail: defaultProps.supportEmail, + }, + id: 'order_confirmation.order_without_support_number_text', + }); }); }); @@ -82,20 +75,15 @@ describe('OrderStatus', () => { }); it('renders message indicating order is pending review', () => { - const orderStatus = mount( - - ); - const translationProps = orderStatus.find('[data-test="order-confirmation-order-status-text"]') + const orderStatus = mount(); + const translationProps = orderStatus + .find('[data-test="order-confirmation-order-status-text"]') .find(TranslatedHtml) .props(); - expect(translationProps) - .toEqual({ - id: 'order_confirmation.order_pending_review_text', - }); + expect(translationProps).toEqual({ + id: 'order_confirmation.order_pending_review_text', + }); }); }); @@ -108,20 +96,15 @@ describe('OrderStatus', () => { }); it('renders message indicating order is awaiting payment', () => { - const orderStatus = mount( - - ); - const translationProps = orderStatus.find('[data-test="order-confirmation-order-status-text"]') + const orderStatus = mount(); + const translationProps = orderStatus + .find('[data-test="order-confirmation-order-status-text"]') .find(TranslatedHtml) .props(); - expect(translationProps) - .toEqual({ - id: 'order_confirmation.order_pending_review_text', - }); + expect(translationProps).toEqual({ + id: 'order_confirmation.order_pending_review_text', + }); }); }); @@ -134,24 +117,19 @@ describe('OrderStatus', () => { }); it('renders message indicating order is pending', () => { - const orderStatus = mount( - - ); - const translationProps = orderStatus.find('[data-test="order-confirmation-order-status-text"]') + const orderStatus = mount(); + const translationProps = orderStatus + .find('[data-test="order-confirmation-order-status-text"]') .find(TranslatedHtml) .props(); - expect(translationProps) - .toEqual({ - data: { - orderNumber: defaultProps.order.orderId, - supportEmail: defaultProps.supportEmail, - }, - id: 'order_confirmation.order_pending_status_text', - }); + expect(translationProps).toEqual({ + data: { + orderNumber: defaultProps.order.orderId, + supportEmail: defaultProps.supportEmail, + }, + id: 'order_confirmation.order_pending_status_text', + }); }); }); @@ -164,24 +142,19 @@ describe('OrderStatus', () => { }); it('renders message indicating order is incomplete', () => { - const orderStatus = mount( - - ); - const translationProps = orderStatus.find('[data-test="order-confirmation-order-status-text"]') + const orderStatus = mount(); + const translationProps = orderStatus + .find('[data-test="order-confirmation-order-status-text"]') .find(TranslatedHtml) .props(); - expect(translationProps) - .toEqual({ - data: { - orderNumber: defaultProps.order.orderId, - supportEmail: defaultProps.supportEmail, - }, - id: 'order_confirmation.order_incomplete_status_text', - }); + expect(translationProps).toEqual({ + data: { + orderNumber: defaultProps.order.orderId, + supportEmail: defaultProps.supportEmail, + }, + id: 'order_confirmation.order_incomplete_status_text', + }); }); }); @@ -195,36 +168,30 @@ describe('OrderStatus', () => { it('renders status with downloadable items text if order is downloadable', () => { const orderStatus = mount( - + , ); - const translationProps = orderStatus.find('[data-test="order-confirmation-digital-items-text"]') + const translationProps = orderStatus + .find('[data-test="order-confirmation-digital-items-text"]') .find(TranslatedHtml) .props(); - expect(translationProps) - .toEqual({ - id: 'order_confirmation.order_with_downloadable_digital_items_text', - }); + expect(translationProps).toEqual({ + id: 'order_confirmation.order_with_downloadable_digital_items_text', + }); }); it('renders status without downloadable items text if order it not yet downloadable', () => { const orderStatus = mount( - + , ); - const translationProps = orderStatus.find('[data-test="order-confirmation-digital-items-text"]') + const translationProps = orderStatus + .find('[data-test="order-confirmation-digital-items-text"]') .find(TranslatedHtml) .props(); - expect(translationProps) - .toEqual({ - id: 'order_confirmation.order_without_downloadable_digital_items_text', - }); + expect(translationProps).toEqual({ + id: 'order_confirmation.order_without_downloadable_digital_items_text', + }); }); }); @@ -234,117 +201,123 @@ describe('OrderStatus', () => { }); it('renders mandate link if it is provided', () => { - const orderStatus = mount( - - ); - - expect(orderStatus.find('[data-test="order-confirmation-mandate-link-text"]') - .prop('href')) - .toEqual('https://www.test.com/mandate'); - expect(orderStatus.find('[data-test="order-confirmation-mandate-id-text"]').length).toEqual(0); + const orderStatus = mount(); + + expect( + orderStatus.find('[data-test="order-confirmation-mandate-link-text"]').prop('href'), + ).toBe('https://www.test.com/mandate'); + expect( + orderStatus.find('[data-test="order-confirmation-mandate-id-text"]'), + ).toHaveLength(0); }); it('renders mandate id if it is provided', () => { order = getOrderWithMandateId(); - const orderStatus = mount( - - ); - expect(orderStatus.find('[data-test="order-confirmation-mandate-link-text"]').length).toEqual(0); - expect(orderStatus.find('[data-test="order-confirmation-mandate-id-text"]').length).toEqual(1); + const orderStatus = mount(); + + expect( + orderStatus.find('[data-test="order-confirmation-mandate-link-text"]'), + ).toHaveLength(0); + expect( + orderStatus.find('[data-test="order-confirmation-mandate-id-text"]'), + ).toHaveLength(1); }); it('renders "SEPA Direct Debit Mandate" text on mandate link when provider description is Stripe (SEPA)', () => { const orderStatus = mount( 295 something', - }, - mandate: { - id: '', - url: 'https://www.test.com/mandate', + payments: [ + { + providerId: 'stripev3', + methodId: 'iban', + description: 'Stripe (SEPA)', + amount: 190, + detail: { + step: 'FINALIZE', + instructions: '295 something', + }, + mandate: { + id: '', + url: 'https://www.test.com/mandate', + }, }, - }], - } } - /> + ], + }} + />, ); - expect(orderStatus.find('[data-test="order-confirmation-mandate-link-text"]') - .text()) - .toEqual('SEPA Direct Debit Mandate'); + expect( + orderStatus.find('[data-test="order-confirmation-mandate-link-text"]').text(), + ).toBe('SEPA Direct Debit Mandate'); }); it('renders "SEPA Direct Debit Mandate" text when provider description is Checkout.com', () => { const orderStatus = mount( 295 something', + payments: [ + { + providerId: 'checkoutcom', + methodId: 'sepa', + description: 'SEPA Direct Debit (via Checkout.com)', + amount: 190, + detail: { + step: 'FINALIZE', + instructions: '295 something', + }, + mandate: { + id: 'ABC123', + url: '', + }, }, - mandate: { - id: 'ABC123', - url: '', - }, - }], - } } - /> + ], + }} + />, ); - expect(orderStatus.find('[data-test="order-confirmation-mandate-id-text"]') - .text()) - .toEqual('SEPA Direct Debit (via Checkout.com) Mandate Reference: ABC123'); + expect( + orderStatus.find('[data-test="order-confirmation-mandate-id-text"]').text(), + ).toBe('SEPA Direct Debit (via Checkout.com) Mandate Reference: ABC123'); }); it('does not render mandate id or url if it is not provided', () => { const orderStatus = mount( 295 something', - }, - mandate: { - id: '', - url: '', + payments: [ + { + providerId: 'stripev3', + methodId: 'iban', + description: 'Stripe (SEPA)', + amount: 190, + detail: { + step: 'FINALIZE', + instructions: '295 something', + }, + mandate: { + id: '', + url: '', + }, }, - }], - } } - /> + ], + }} + />, ); - expect(orderStatus.find('[data-test="order-confirmation-mandate-link-text"]').length).toEqual(0); - expect(orderStatus.find('[data-test="order-confirmation-mandate-id-text"]').length).toEqual(0); + expect( + orderStatus.find('[data-test="order-confirmation-mandate-link-text"]'), + ).toHaveLength(0); + expect( + orderStatus.find('[data-test="order-confirmation-mandate-id-text"]'), + ).toHaveLength(0); }); }); - }); diff --git a/packages/core/src/app/order/OrderStatus.tsx b/packages/core/src/app/order/OrderStatus.tsx index 7b71b3562f..7da039c987 100644 --- a/packages/core/src/app/order/OrderStatus.tsx +++ b/packages/core/src/app/order/OrderStatus.tsx @@ -1,5 +1,5 @@ import { GatewayOrderPayment, GiftCertificateOrderPayment, Order } from '@bigcommerce/checkout-sdk'; -import React, { memo, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo } from 'react'; import { TranslatedHtml, TranslatedString } from '../locale'; @@ -11,64 +11,82 @@ export interface OrderStatusProps { order: Order; } -type PaymentWithMandate = GatewayOrderPayment & Required>; +type PaymentWithMandate = GatewayOrderPayment & + Required>; -const isPaymentWithMandate = (payment: GatewayOrderPayment | GiftCertificateOrderPayment): payment is PaymentWithMandate => - !!payment.methodId && ('mandate' in payment && !!payment.mandate); +const isPaymentWithMandate = ( + payment: GatewayOrderPayment | GiftCertificateOrderPayment, +): payment is PaymentWithMandate => !!payment.methodId && 'mandate' in payment && !!payment.mandate; const OrderStatus: FunctionComponent = ({ order, supportEmail, supportPhoneNumber, }) => { - const paymentsWithMandates = order.payments?.filter(isPaymentWithMandate) || []; - return - { order.orderId && -

    - -

    } - -

    - -

    - { - paymentsWithMandates.map(payment => { + return ( + + {order.orderId && ( +

    + +

    + )} + +

    + +

    + {paymentsWithMandates.map((payment) => { if (payment.mandate.url) { - return + return ( + - ; + + ); } else if (payment.mandate.id) { - return

    - -

    ; + return ( +

    + +

    + ); } - }) - } - - { order.hasDigitalItems && -

    - -

    } -
    ; + })} + + {order.hasDigitalItems && ( +

    + +

    + )} +
    + ); }; interface OrderStatusMessageProps { @@ -85,31 +103,37 @@ const OrderStatusMessage: FunctionComponent = ({ supportPhoneNumber, }) => { switch (orderStatus) { - case 'MANUAL_VERIFICATION_REQUIRED': - case 'AWAITING_PAYMENT': - return ; - - case 'PENDING': - return ; - - case 'INCOMPLETE': - return ; - - default: - return ; + case 'MANUAL_VERIFICATION_REQUIRED': + case 'AWAITING_PAYMENT': + return ; + + case 'PENDING': + return ( + + ); + + case 'INCOMPLETE': + return ( + + ); + + default: + return ( + + ); } }; diff --git a/packages/core/src/app/order/OrderSummary.spec.tsx b/packages/core/src/app/order/OrderSummary.spec.tsx index 8e4c03f9f7..61645bbe00 100644 --- a/packages/core/src/app/order/OrderSummary.spec.tsx +++ b/packages/core/src/app/order/OrderSummary.spec.tsx @@ -17,14 +17,16 @@ describe('OrderSummary', () => { beforeEach(() => { order = getOrder(); - orderSummary = shallow( } - lineItems={ order.lineItems } - shopperCurrency={ getStoreConfig().shopperCurrency } - storeCurrency={ getStoreConfig().currency } - total={ order.orderAmount } - />); + orderSummary = shallow( + } + lineItems={order.lineItems} + shopperCurrency={getStoreConfig().shopperCurrency} + storeCurrency={getStoreConfig().currency} + total={order.orderAmount} + />, + ); }); it('renders order summary', () => { @@ -32,7 +34,7 @@ describe('OrderSummary', () => { }); it('does not render currency cart note', () => { - expect(orderSummary.find('.cart-note').length).toEqual(0); + expect(orderSummary.find('.cart-note')).toHaveLength(0); }); }); }); diff --git a/packages/core/src/app/order/OrderSummary.tsx b/packages/core/src/app/order/OrderSummary.tsx index c3a5c46f11..eaeb99b066 100644 --- a/packages/core/src/app/order/OrderSummary.tsx +++ b/packages/core/src/app/order/OrderSummary.tsx @@ -1,12 +1,12 @@ import { LineItemMap, ShopperCurrency, StoreCurrency } from '@bigcommerce/checkout-sdk'; -import React, { useMemo, FunctionComponent, ReactNode } from 'react'; +import React, { FunctionComponent, ReactNode, useMemo } from 'react'; -import removeBundledItems from './removeBundledItems'; import OrderSummaryHeader from './OrderSummaryHeader'; import OrderSummaryItems from './OrderSummaryItems'; import OrderSummarySection from './OrderSummarySection'; import OrderSummarySubtotals, { OrderSummarySubtotalsProps } from './OrderSummarySubtotals'; import OrderSummaryTotal from './OrderSummaryTotal'; +import removeBundledItems from './removeBundledItems'; export interface OrderSummaryProps { lineItems: LineItemMap; @@ -26,34 +26,30 @@ const OrderSummary: FunctionComponent { - const nonBundledLineItems = useMemo(() => ( - removeBundledItems(lineItems) - ), [lineItems]); - - return
    - - { headerLink } - - - - - - - - - { additionalLineItems } - - - - - -
    ; + const nonBundledLineItems = useMemo(() => removeBundledItems(lineItems), [lineItems]); + + return ( +
    + {headerLink} + + + + + + + + {additionalLineItems} + + + + + +
    + ); }; export default OrderSummary; diff --git a/packages/core/src/app/order/OrderSummaryDiscount.spec.tsx b/packages/core/src/app/order/OrderSummaryDiscount.spec.tsx index 10a507df95..033d7bca69 100644 --- a/packages/core/src/app/order/OrderSummaryDiscount.spec.tsx +++ b/packages/core/src/app/order/OrderSummaryDiscount.spec.tsx @@ -15,35 +15,31 @@ describe('OrderSummaryDiscount', () => { beforeEach(() => { localeContext = createLocaleContext(getStoreConfig()); discount = mount( - - Foo } - /> - + + Foo} /> + , ); }); it('passes right props to OrderSummaryPrice', () => { - expect(discount.find(OrderSummaryPrice).props()) - .toMatchObject({ - amount: -10, - label: Foo, - }); + expect(discount.find(OrderSummaryPrice).props()).toMatchObject({ + amount: -10, + label: Foo, + }); }); }); describe('when discount has code and remaining balance', () => { beforeEach(() => { discount = mount( - + - + , ); }); @@ -52,18 +48,19 @@ describe('OrderSummaryDiscount', () => { }); it('renders gift certificate code', () => { - expect(discount.find('[data-test="cart-price-code"]').text()) - .toEqual('ABCDFE'); + expect(discount.find('[data-test="cart-price-code"]').text()).toBe('ABCDFE'); }); it('renders remaining label', () => { - expect(discount.find('[data-test="cart-price-remaining"]').text()) - .toContain('Remaining:'); + expect(discount.find('[data-test="cart-price-remaining"]').text()).toContain( + 'Remaining:', + ); }); it('renders remaining balance', () => { - expect(discount.find('[data-test="cart-price-remaining"] ShopperCurrency').props()) - .toMatchObject({ amount: 2 }); + expect( + discount.find('[data-test="cart-price-remaining"] ShopperCurrency').props(), + ).toMatchObject({ amount: 2 }); }); }); }); diff --git a/packages/core/src/app/order/OrderSummaryDiscount.tsx b/packages/core/src/app/order/OrderSummaryDiscount.tsx index 9449150cbe..7e0bd0c596 100644 --- a/packages/core/src/app/order/OrderSummaryDiscount.tsx +++ b/packages/core/src/app/order/OrderSummaryDiscount.tsx @@ -1,4 +1,4 @@ -import React, { memo, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo } from 'react'; import { ShopperCurrency } from '../currency'; import { TranslatedString } from '../locale'; @@ -19,28 +19,32 @@ const OrderSummaryDiscount: FunctionComponent = ({ ...rest }) => ( code && onRemoved(code), actionLabel: , - }) } - amount={ -1 * (amount || 0) } + })} + amount={-1 * (amount || 0)} > - { !!remaining && remaining > 0 && - - { ': ' } - - } + {!!remaining && remaining > 0 && ( + + + {': '} + + + )} - { code && - { code } - } + {code && ( + + {code} + + )} ); diff --git a/packages/core/src/app/order/OrderSummaryDrawer.spec.tsx b/packages/core/src/app/order/OrderSummaryDrawer.spec.tsx index 9ccd9561b7..ee48d0c2a5 100644 --- a/packages/core/src/app/order/OrderSummaryDrawer.spec.tsx +++ b/packages/core/src/app/order/OrderSummaryDrawer.spec.tsx @@ -26,56 +26,56 @@ describe('OrderSummaryDrawer', () => { order = getOrder(); orderSummary = mount( - + } - lineItems={ order.lineItems } - shopperCurrency={ getStoreConfig().shopperCurrency } - storeCurrency={ getStoreConfig().currency } - total={ order.orderAmount } + headerLink={} + lineItems={order.lineItems} + shopperCurrency={getStoreConfig().shopperCurrency} + storeCurrency={getStoreConfig().currency} + total={order.orderAmount} /> - + , ); }); it('renders gift certificate icon when buying only GC', () => { orderSummary = mount( - + } - lineItems={ { - giftCertificates: [ getGiftCertificateItem() ], + headerLink={} + lineItems={{ + giftCertificates: [getGiftCertificateItem()], physicalItems: [], digitalItems: [], - } } - shopperCurrency={ getStoreConfig().shopperCurrency } - storeCurrency={ getStoreConfig().currency } - total={ order.orderAmount } + }} + shopperCurrency={getStoreConfig().shopperCurrency} + storeCurrency={getStoreConfig().currency} + total={order.orderAmount} /> - + , ); - expect(orderSummary.find(IconGiftCertificate).length) - .toEqual(1); + expect(orderSummary.find(IconGiftCertificate)).toHaveLength(1); }); it('renders order amount', () => { - expect(orderSummary.find(ShopperCurrency).prop('amount')) - .toEqual(order.orderAmount); + expect(orderSummary.find(ShopperCurrency).prop('amount')).toEqual(order.orderAmount); }); it('renders line items count', () => { - expect(orderSummary.find('.cartDrawer-items').text()) - .toEqual(localeContext.language.translate('cart.item_count_text', { count: 2 })); + expect(orderSummary.find('.cartDrawer-items').text()).toEqual( + localeContext.language.translate('cart.item_count_text', { count: 2 }), + ); }); it('renders image of first product', () => { - expect(orderSummary.find('[data-test="cart-item-image"]').prop('src')) - .toEqual(getPhysicalItem().imageUrl); + expect(orderSummary.find('[data-test="cart-item-image"]').prop('src')).toEqual( + getPhysicalItem().imageUrl, + ); }); describe('when clicked', () => { @@ -84,51 +84,43 @@ describe('OrderSummaryDrawer', () => { }); it('renders order summary modal with the right props', () => { - expect(orderSummary.find(OrderSummaryModal).length) - .toEqual(1); - - expect(orderSummary.find(OrderSummaryModal).find('#cart-print-link').length) - .toEqual(1); - - expect(orderSummary.find(OrderSummaryModal).props()) - .toMatchObject({ - ...mapToOrderSummarySubtotalsProps(getOrder()), - lineItems: getOrder().lineItems, - total: getOrder().orderAmount, - storeCurrency: getStoreConfig().currency, - shopperCurrency: getStoreConfig().shopperCurrency, - additionalLineItems: 'foo', - }); - - expect(document.body.scrollIntoView) - .toBeCalled(); + expect(orderSummary.find(OrderSummaryModal)).toHaveLength(1); + + expect(orderSummary.find(OrderSummaryModal).find('#cart-print-link')).toHaveLength(1); + + expect(orderSummary.find(OrderSummaryModal).props()).toMatchObject({ + ...mapToOrderSummarySubtotalsProps(getOrder()), + lineItems: getOrder().lineItems, + total: getOrder().orderAmount, + storeCurrency: getStoreConfig().currency, + shopperCurrency: getStoreConfig().shopperCurrency, + additionalLineItems: 'foo', + }); + + expect(document.body.scrollIntoView).toHaveBeenCalled(); }); }); describe('when active and enter is pressed', () => { beforeEach(() => { - orderSummary.find('.cartDrawer').simulate('keypress', {key: 'Enter'}); + orderSummary.find('.cartDrawer').simulate('keypress', { key: 'Enter' }); }); it('renders order summary modal with the right props', () => { - expect(orderSummary.find(OrderSummaryModal).length) - .toEqual(1); - - expect(orderSummary.find(OrderSummaryModal).find('#cart-print-link').length) - .toEqual(1); - - expect(orderSummary.find(OrderSummaryModal).props()) - .toMatchObject({ - ...mapToOrderSummarySubtotalsProps(getOrder()), - lineItems: getOrder().lineItems, - total: getOrder().orderAmount, - storeCurrency: getStoreConfig().currency, - shopperCurrency: getStoreConfig().shopperCurrency, - additionalLineItems: 'foo', - }); - - expect(document.body.scrollIntoView) - .toBeCalled(); + expect(orderSummary.find(OrderSummaryModal)).toHaveLength(1); + + expect(orderSummary.find(OrderSummaryModal).find('#cart-print-link')).toHaveLength(1); + + expect(orderSummary.find(OrderSummaryModal).props()).toMatchObject({ + ...mapToOrderSummarySubtotalsProps(getOrder()), + lineItems: getOrder().lineItems, + total: getOrder().orderAmount, + storeCurrency: getStoreConfig().currency, + shopperCurrency: getStoreConfig().shopperCurrency, + additionalLineItems: 'foo', + }); + + expect(document.body.scrollIntoView).toHaveBeenCalled(); }); }); }); diff --git a/packages/core/src/app/order/OrderSummaryDrawer.tsx b/packages/core/src/app/order/OrderSummaryDrawer.tsx index bbb2371595..1d124bc872 100644 --- a/packages/core/src/app/order/OrderSummaryDrawer.tsx +++ b/packages/core/src/app/order/OrderSummaryDrawer.tsx @@ -1,6 +1,10 @@ -import { LineItemMap, ShopperCurrency as ShopperCurrencyType, StoreCurrency } from '@bigcommerce/checkout-sdk'; +import { + LineItemMap, + ShopperCurrency as ShopperCurrencyType, + StoreCurrency, +} from '@bigcommerce/checkout-sdk'; import classNames from 'classnames'; -import React, { memo, useCallback, FunctionComponent, ReactNode } from 'react'; +import React, { FunctionComponent, memo, ReactNode, useCallback } from 'react'; import { ShopperCurrency } from '../currency'; import { TranslatedString } from '../locale'; @@ -21,7 +25,9 @@ export interface OrderSummaryDrawerProps { additionalLineItems?: ReactNode; } -const OrderSummaryDrawer: FunctionComponent = ({ +const OrderSummaryDrawer: FunctionComponent< + OrderSummaryDrawerProps & OrderSummarySubtotalsProps +> = ({ additionalLineItems, coupons, discountAmount, @@ -40,93 +46,99 @@ const OrderSummaryDrawer: FunctionComponent { - const renderModal = useCallback(props => ( - - ), [ - additionalLineItems, - coupons, - discountAmount, - giftCertificates, - handlingAmount, - headerLink, - lineItems, - onRemovedCoupon, - onRemovedGiftCertificate, - giftWrappingAmount, - shippingAmount, - shopperCurrency, - storeCreditAmount, - storeCurrency, - subtotalAmount, - taxes, - total, - ]); + const renderModal = useCallback( + (props) => ( + + ), + [ + additionalLineItems, + coupons, + discountAmount, + giftCertificates, + handlingAmount, + headerLink, + lineItems, + onRemovedCoupon, + onRemovedGiftCertificate, + giftWrappingAmount, + shippingAmount, + shopperCurrency, + storeCreditAmount, + storeCurrency, + subtotalAmount, + taxes, + total, + ], + ); - return - { ({ onClick, onKeyPress }) =>
    -
    1 } - ) } - > -
    - { getImage(lineItems) } + return ( + + {({ onClick, onKeyPress }) => ( +
    +
    1, + })} + > +
    {getImage(lineItems)}
    +
    +
    +

    + +

    + + + +
    +
    +

    + +

    +
    -
    -
    -

    - -

    - - - -
    -
    -

    - -

    -
    -
    } -
    ; + )} + + ); }; function getImage(lineItems: LineItemMap): ReactNode { const productWithImage = lineItems.physicalItems[0] || lineItems.digitalItems[0]; if (productWithImage && productWithImage.imageUrl) { - return {; + return ( + {productWithImage.name} + ); } if (lineItems.giftCertificates.length) { diff --git a/packages/core/src/app/order/OrderSummaryHeader.tsx b/packages/core/src/app/order/OrderSummaryHeader.tsx index 8609923e63..ae5ab3b622 100644 --- a/packages/core/src/app/order/OrderSummaryHeader.tsx +++ b/packages/core/src/app/order/OrderSummaryHeader.tsx @@ -7,7 +7,7 @@ const OrderSummaryHeader: FunctionComponent = ({ children }) => (

    - { children } + {children} ); diff --git a/packages/core/src/app/order/OrderSummaryItem.spec.tsx b/packages/core/src/app/order/OrderSummaryItem.spec.tsx index dd3a975974..fd863411be 100644 --- a/packages/core/src/app/order/OrderSummaryItem.spec.tsx +++ b/packages/core/src/app/order/OrderSummaryItem.spec.tsx @@ -8,18 +8,22 @@ describe('OrderSummaryItems', () => { describe('when amount and amountAfterDiscount are different', () => { beforeEach(() => { - orderSummaryItem = shallow( } - name="Product" - productOptions={ [{ - testId: 'test-id', - content: , - }] } - quantity={ 2 } - />); + orderSummaryItem = shallow( + } + name="Product" + productOptions={[ + { + testId: 'test-id', + content: , + }, + ]} + quantity={2} + />, + ); }); it('renders component', () => { @@ -29,61 +33,67 @@ describe('OrderSummaryItems', () => { describe('when no discount is present', () => { beforeEach(() => { - orderSummaryItem = shallow(); + orderSummaryItem = shallow( + , + ); }); it('does not render discount', () => { - expect(orderSummaryItem.find('[data-test="cart-item-product-price--afterDiscount"]').length) - .toEqual(0); + expect( + orderSummaryItem.find('[data-test="cart-item-product-price--afterDiscount"]'), + ).toHaveLength(0); }); it('does not apply strike to original amount', () => { - expect(orderSummaryItem.find('[data-test="cart-item-product-price"]').props().className) - .not.toContain('product-price--beforeDiscount'); + expect( + orderSummaryItem.find('[data-test="cart-item-product-price"]').props().className, + ).not.toContain('product-price--beforeDiscount'); }); }); describe('when amount and amountAfterDiscount are the same', () => { beforeEach(() => { - orderSummaryItem = shallow(); + orderSummaryItem = shallow( + , + ); }); it('does not render discount', () => { - expect(orderSummaryItem.find('[data-test="cart-item-product-price--afterDiscount"]').length) - .toEqual(0); + expect( + orderSummaryItem.find('[data-test="cart-item-product-price--afterDiscount"]'), + ).toHaveLength(0); }); it('does not apply strike to original amount', () => { - expect(orderSummaryItem.find('[data-test="cart-item-product-price"]').props().className) - .not.toContain('product-price--beforeDiscount'); + expect( + orderSummaryItem.find('[data-test="cart-item-product-price"]').props().className, + ).not.toContain('product-price--beforeDiscount'); }); }); describe('when description is present', () => { beforeEach(() => { - orderSummaryItem = shallow(); + orderSummaryItem = shallow( + , + ); }); it('does render description', () => { - expect(orderSummaryItem.find('[data-test="cart-item-product-description"]').text()) - .toEqual('Description'); + expect( + orderSummaryItem.find('[data-test="cart-item-product-description"]').text(), + ).toBe('Description'); }); }); }); diff --git a/packages/core/src/app/order/OrderSummaryItem.tsx b/packages/core/src/app/order/OrderSummaryItem.tsx index 656c4938bf..aaffa3879c 100644 --- a/packages/core/src/app/order/OrderSummaryItem.tsx +++ b/packages/core/src/app/order/OrderSummaryItem.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import { isNumber } from 'lodash'; -import React, { memo, FunctionComponent, ReactNode } from 'react'; +import React, { FunctionComponent, memo, ReactNode } from 'react'; import { ShopperCurrency } from '../currency'; @@ -30,58 +30,53 @@ const OrderSummaryItem: FunctionComponent = ({ description, }) => (
    -
    - { image } -
    +
    {image}

    - { `${quantity} x ${name}` } + {`${quantity} x ${name}`}

    - { (productOptions && productOptions.length>0) &&
      0 && ( +
        - { productOptions.map((option, index) => -
      • - { option.content } + {productOptions.map((option, index) => ( +
      • + {option.content}
      • - ) } + ))}
      - } - { description &&
      - { description } -
      } + )} + {description && ( +
      + {description} +
      + )}
    - +
    - { isNumber(amountAfterDiscount) && amountAfterDiscount !== amount &&
    - -
    } + {isNumber(amountAfterDiscount) && amountAfterDiscount !== amount && ( +
    + +
    + )}
    ); diff --git a/packages/core/src/app/order/OrderSummaryItems.spec.tsx b/packages/core/src/app/order/OrderSummaryItems.spec.tsx index 6c29df2ff6..29dc482072 100644 --- a/packages/core/src/app/order/OrderSummaryItems.spec.tsx +++ b/packages/core/src/app/order/OrderSummaryItems.spec.tsx @@ -1,7 +1,12 @@ -import { mount, shallow, ReactWrapper, ShallowWrapper } from 'enzyme'; +import { mount, ReactWrapper, shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; -import { getCustomItem, getDigitalItem, getGiftCertificateItem, getPhysicalItem } from '../cart/lineItem.mock'; +import { + getCustomItem, + getDigitalItem, + getGiftCertificateItem, + getPhysicalItem, +} from '../cart/lineItem.mock'; import { getStoreConfig } from '../config/config.mock'; import { createLocaleContext, LocaleContext, LocaleContextType, TranslatedString } from '../locale'; @@ -12,27 +17,27 @@ describe('OrderSummaryItems', () => { let orderSummaryItems: ShallowWrapper; beforeEach(() => { - orderSummaryItems = shallow(); + orderSummaryItems = shallow( + , + ); }); it('renders total count', () => { - expect(orderSummaryItems.find(TranslatedString).props()) - .toMatchObject({ - id: 'cart.item_count_text', - data: { count: 5 }, - }); + expect(orderSummaryItems.find(TranslatedString).props()).toMatchObject({ + id: 'cart.item_count_text', + data: { count: 5 }, + }); }); it('renders product list', () => { - expect(orderSummaryItems.find('.productList')) - .toMatchSnapshot(); + expect(orderSummaryItems.find('.productList')).toMatchSnapshot(); }); it('does not render actions', () => { @@ -48,62 +53,55 @@ describe('OrderSummaryItems', () => { localeContext = createLocaleContext(getStoreConfig()); orderSummaryItems = mount( - - - ); + + + , + ); }); it('renders actions', () => { - expect(orderSummaryItems.find('.cart-actions')) - .toHaveLength(1); + expect(orderSummaryItems.find('.cart-actions')).toHaveLength(1); }); it('is collapsed by default', () => { - expect(orderSummaryItems.find('.cart-actions').text()) - .toMatch('See All'); + expect(orderSummaryItems.find('.cart-actions').text()).toMatch('See All'); - expect(orderSummaryItems.find('.productList-item').length) - .toEqual(4); + expect(orderSummaryItems.find('.productList-item')).toHaveLength(4); }); describe('when action is clicked', () => { beforeEach(() => { - orderSummaryItems.find('.cart-actions button') - .simulate('click'); + orderSummaryItems.find('.cart-actions button').simulate('click'); }); it('shows the rest of the items', () => { - expect(orderSummaryItems.find('.cart-actions').text()) - .toMatch('See Less'); + expect(orderSummaryItems.find('.cart-actions').text()).toMatch('See Less'); - expect(orderSummaryItems.find('.productList-item').length) - .toEqual(5); + expect(orderSummaryItems.find('.productList-item')).toHaveLength(5); }); describe('when action is clicked a second time', () => { beforeEach(() => { - orderSummaryItems.find('.cart-actions button') - .simulate('click'); + orderSummaryItems.find('.cart-actions button').simulate('click'); }); it('collapses line items back', () => { - expect(orderSummaryItems.find('.cart-actions').text()) - .toMatch('See All'); + expect(orderSummaryItems.find('.cart-actions').text()).toMatch('See All'); - expect(orderSummaryItems.find('.productList-item').length) - .toEqual(4); + expect(orderSummaryItems.find('.productList-item')).toHaveLength(4); }); }); }); diff --git a/packages/core/src/app/order/OrderSummaryItems.tsx b/packages/core/src/app/order/OrderSummaryItems.tsx index 7d0b518dc8..8bce06076c 100644 --- a/packages/core/src/app/order/OrderSummaryItems.tsx +++ b/packages/core/src/app/order/OrderSummaryItems.tsx @@ -1,5 +1,5 @@ import { LineItemMap } from '@bigcommerce/checkout-sdk'; -import React, { Fragment, ReactNode } from 'react'; +import React, { ReactNode } from 'react'; import { TranslatedString } from '../locale'; import { IconChevronDown, IconChevronUp } from '../ui/icon'; @@ -34,48 +34,42 @@ class OrderSummaryItems extends React.Component -

    - -

    - -
      - { - [ + return ( + <> +

      + +

      + +
        + {[ ...items.physicalItems .slice() - .sort(item => item.variantId) + .sort((item) => item.variantId) .map(mapFromPhysical), - ...items.giftCertificates - .slice() - .map(mapFromGiftCertificate), + ...items.giftCertificates.slice().map(mapFromGiftCertificate), ...items.digitalItems .slice() - .sort(item => item.variantId) + .sort((item) => item.variantId) .map(mapFromDigital), - ...(items.customItems || []) - .map(mapFromCustom), + ...(items.customItems || []).map(mapFromCustom), ] .slice(0, isExpanded ? undefined : COLLAPSED_ITEMS_LIMIT) - .map(summaryItemProps => -
      • - + .map((summaryItemProps) => ( +
      • +
      • - ) - } -
      + ))} +
    - { this.renderActions() } - ); + {this.renderActions()} + + ); } private renderActions(): ReactNode { @@ -89,18 +83,20 @@ class OrderSummaryItems extends React.Component
    ); @@ -109,10 +105,12 @@ class OrderSummaryItems extends React.Component void = () => { diff --git a/packages/core/src/app/order/OrderSummaryModal.spec.tsx b/packages/core/src/app/order/OrderSummaryModal.spec.tsx index 540ad4f09d..5df6637f14 100644 --- a/packages/core/src/app/order/OrderSummaryModal.spec.tsx +++ b/packages/core/src/app/order/OrderSummaryModal.spec.tsx @@ -15,15 +15,17 @@ describe('OrderSummaryModal', () => { beforeEach(() => { order = getOrder(); - orderSummary = shallow(); + orderSummary = shallow( + , + ); }); it('renders order summary', () => { diff --git a/packages/core/src/app/order/OrderSummaryModal.tsx b/packages/core/src/app/order/OrderSummaryModal.tsx index 72d671dff1..5f0d1ce0d1 100644 --- a/packages/core/src/app/order/OrderSummaryModal.tsx +++ b/packages/core/src/app/order/OrderSummaryModal.tsx @@ -1,5 +1,9 @@ -import { LineItemMap, ShopperCurrency as ShopperCurrencyType, StoreCurrency } from '@bigcommerce/checkout-sdk'; -import React, { Fragment, FunctionComponent, ReactNode } from 'react'; +import { + LineItemMap, + ShopperCurrency as ShopperCurrencyType, + StoreCurrency, +} from '@bigcommerce/checkout-sdk'; +import React, { FunctionComponent, ReactNode } from 'react'; import { preventDefault } from '../common/dom'; import { TranslatedString } from '../locale'; @@ -23,7 +27,9 @@ export interface OrderSummaryDrawerProps { onAfterOpen?(): void; } -const OrderSummaryModal: FunctionComponent = ({ +const OrderSummaryModal: FunctionComponent< + OrderSummaryDrawerProps & OrderSummarySubtotalsProps +> = ({ additionalLineItems, children, onRequestClose, @@ -36,55 +42,48 @@ const OrderSummaryModal: FunctionComponent ( - - - - - - - { additionalLineItems } - - - - - + + + + + + + {additionalLineItems} + + + + + ); const renderHeader: FunctionComponent<{ headerLink: ReactNode; onRequestClose?(): void; -}> = ({ - onRequestClose, - headerLink, -}) => ( - - - - - - - - - +}> = ({ onRequestClose, headerLink }) => ( + <> + + + + + + + + + - { headerLink } -); + {headerLink} + +); export default OrderSummaryModal; diff --git a/packages/core/src/app/order/OrderSummaryPrice.spec.tsx b/packages/core/src/app/order/OrderSummaryPrice.spec.tsx index a8c4f8b839..24ff6701f0 100644 --- a/packages/core/src/app/order/OrderSummaryPrice.spec.tsx +++ b/packages/core/src/app/order/OrderSummaryPrice.spec.tsx @@ -13,12 +13,11 @@ describe('OrderSummaryPrice', () => { describe('and has only required props', () => { beforeEach(() => { - orderSummaryPrice = shallow( - Foo Children - ); + orderSummaryPrice = shallow( + + Foo Children + , + ); }); it('renders component', () => { @@ -28,45 +27,45 @@ describe('OrderSummaryPrice', () => { describe('and has only required props', () => { beforeEach(() => { - orderSummaryPrice = shallow(); + orderSummaryPrice = shallow( + , + ); }); it('renders additional elements/props', () => { - expect(orderSummaryPrice.first().props()) - .toMatchObject({ - 'data-test': 'test-id', - }); + expect(orderSummaryPrice.first().props()).toMatchObject({ + 'data-test': 'test-id', + }); - expect(orderSummaryPrice.find('.cart-priceItem').props().className) - .toContain('extra-class'); + expect(orderSummaryPrice.find('.cart-priceItem').props().className).toContain( + 'extra-class', + ); - expect(orderSummaryPrice.find('.cart-priceItem-currencyCode').text()) - .toMatch('(EUR)'); + expect(orderSummaryPrice.find('.cart-priceItem-currencyCode').text()).toMatch( + '(EUR)', + ); - expect(orderSummaryPrice.find('[data-test="cart-price-value-superscript"]').text()) - .toMatch('*'); + expect( + orderSummaryPrice.find('[data-test="cart-price-value-superscript"]').text(), + ).toMatch('*'); }); }); }); describe('when has null amount', () => { beforeEach(() => { - orderSummaryPrice = shallow(); + orderSummaryPrice = shallow(); }); it('renders not yet symbol as label', () => { - expect(orderSummaryPrice.find('[data-test="cart-price-value"]').text()) - .toEqual('--'); + expect(orderSummaryPrice.find('[data-test="cart-price-value"]').text()).toBe('--'); }); }); @@ -75,34 +74,40 @@ describe('OrderSummaryPrice', () => { describe('and no label', () => { beforeEach(() => { - orderSummaryPrice = shallow(); + orderSummaryPrice = shallow( + , + ); }); it('renders formatted amount', () => { - expect(orderSummaryPrice.find(ShopperCurrency).props()) - .toMatchObject({ amount: 0 }); + expect(orderSummaryPrice.find(ShopperCurrency).props()).toMatchObject({ + amount: 0, + }); }); }); describe('and zero label', () => { beforeEach(() => { - orderSummaryPrice = shallow(); + orderSummaryPrice = shallow( + , + ); }); it('renders zero label', () => { - expect(orderSummaryPrice.find('[data-test="cart-price-value"]').text()) - .toEqual('Free'); + expect(orderSummaryPrice.find('[data-test="cart-price-value"]').text()).toBe( + 'Free', + ); }); }); }); diff --git a/packages/core/src/app/order/OrderSummaryPrice.tsx b/packages/core/src/app/order/OrderSummaryPrice.tsx index 105145569a..e00d0ec584 100644 --- a/packages/core/src/app/order/OrderSummaryPrice.tsx +++ b/packages/core/src/app/order/OrderSummaryPrice.tsx @@ -71,53 +71,59 @@ class OrderSummaryPrice extends Component +
    - { label } - { ' ' } + {label} + {' '} - { currencyCode && - { `(${currencyCode}) ` } - } - { onActionTriggered && actionLabel && - - { actionLabel } - - } + {currencyCode && ( + + {`(${currencyCode}) `} + + )} + {onActionTriggered && actionLabel && ( + + + {actionLabel} + + + )} - { isNumberValue(displayValue) ? - : - displayValue } + {isNumberValue(displayValue) ? ( + + ) : ( + displayValue + )} - { superscript && - { superscript } - } + {superscript && ( + {superscript} + )} - { children } + {children}
    diff --git a/packages/core/src/app/order/OrderSummarySection.tsx b/packages/core/src/app/order/OrderSummarySection.tsx index 397c998636..884694fede 100644 --- a/packages/core/src/app/order/OrderSummarySection.tsx +++ b/packages/core/src/app/order/OrderSummarySection.tsx @@ -1,10 +1,8 @@ import React, { FunctionComponent } from 'react'; -const OrderSummarySection: FunctionComponent = ({ - children, -}) => ( +const OrderSummarySection: FunctionComponent = ({ children }) => (
    - { children } + {children}
    ); diff --git a/packages/core/src/app/order/OrderSummarySubtotals.spec.tsx b/packages/core/src/app/order/OrderSummarySubtotals.spec.tsx index 801ddb18da..b3134b7b97 100644 --- a/packages/core/src/app/order/OrderSummarySubtotals.spec.tsx +++ b/packages/core/src/app/order/OrderSummarySubtotals.spec.tsx @@ -12,15 +12,17 @@ describe('OrderSummarySubtotals', () => { beforeEach(() => { const order = getOrder(); - orderSummarySubtotals = shallow(); + orderSummarySubtotals = shallow( + , + ); }); it('renders component', () => { diff --git a/packages/core/src/app/order/OrderSummarySubtotals.tsx b/packages/core/src/app/order/OrderSummarySubtotals.tsx index 8c61af5b7e..f3da032637 100644 --- a/packages/core/src/app/order/OrderSummarySubtotals.tsx +++ b/packages/core/src/app/order/OrderSummarySubtotals.tsx @@ -1,5 +1,5 @@ import { Coupon, GiftCertificate, Tax } from '@bigcommerce/checkout-sdk'; -import React, { memo, Fragment, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo } from 'react'; import { TranslatedString } from '../locale'; @@ -33,80 +33,87 @@ const OrderSummarySubtotals: FunctionComponent = ({ onRemovedGiftCertificate, onRemovedCoupon, }) => { - return ( - } - testId="cart-subtotal" - /> + return ( + <> + } + testId="cart-subtotal" + /> - { (coupons || []) - .map((coupon, index) => + {(coupons || []).map((coupon, index) => ( - ) } + ))} - { !!discountAmount && } - testId="cart-discount" - /> } + {!!discountAmount && ( + } + testId="cart-discount" + /> + )} - { (giftCertificates || []) - .map((giftCertificate, index) => + {(giftCertificates || []).map((giftCertificate, index) => ( } - onRemoved={ onRemovedGiftCertificate } - remaining={ giftCertificate.remaining } + amount={giftCertificate.used} + code={giftCertificate.code} + key={index} + label={} + onRemoved={onRemovedGiftCertificate} + remaining={giftCertificate.remaining} testId="cart-gift-certificate" /> - ) } + ))} - { !!giftWrappingAmount && } - testId="cart-gift-wrapping" - /> } + {!!giftWrappingAmount && ( + } + testId="cart-gift-wrapping" + /> + )} - } - testId="cart-shipping" - zeroLabel={ } - /> + } + testId="cart-shipping" + zeroLabel={} + /> - { !!handlingAmount && } - testId="cart-handling" - /> } + {!!handlingAmount && ( + } + testId="cart-handling" + /> + )} - { (taxes || []) - .map((tax, index) => + {(taxes || []).map((tax, index) => ( - ) } + ))} - { !!storeCreditAmount && } - testId="cart-store-credit" - /> } - ); + {!!storeCreditAmount && ( + } + testId="cart-store-credit" + /> + )} + + ); }; export default memo(OrderSummarySubtotals); diff --git a/packages/core/src/app/order/OrderSummaryTotal.spec.tsx b/packages/core/src/app/order/OrderSummaryTotal.spec.tsx index 7b9c1683b0..30911cd39a 100644 --- a/packages/core/src/app/order/OrderSummaryTotal.spec.tsx +++ b/packages/core/src/app/order/OrderSummaryTotal.spec.tsx @@ -18,63 +18,69 @@ describe('OrderSummaryTotal', () => { describe('when shopper has same currency as store', () => { beforeEach(() => { orderSummaryTotal = mount( - + - ); + , + ); }); it('passes right props to OrderSummaryPrice', () => { - expect(orderSummaryTotal.find(OrderSummaryPrice).props()) - .toMatchObject({ - amount: 100, - superscript: undefined, - }); + expect(orderSummaryTotal.find(OrderSummaryPrice).props()).toMatchObject({ + amount: 100, + superscript: undefined, + }); }); it('displays total in store currency', () => { - expect(orderSummaryTotal.find('[data-test="cart-total"]').text()) - .toEqual('Total (USD) $112.00'); + expect(orderSummaryTotal.find('[data-test="cart-total"]').text()).toBe( + 'Total (USD) $112.00', + ); }); }); describe('when shopper has different currency as store', () => { beforeEach(() => { orderSummaryTotal = mount( - + - ); + , + ); }); it('passes right props to OrderSummaryPrice', () => { - expect(orderSummaryTotal.find(OrderSummaryPrice).props()) - .toMatchObject({ - amount: 100, - superscript: '*', - }); + expect(orderSummaryTotal.find(OrderSummaryPrice).props()).toMatchObject({ + amount: 100, + superscript: '*', + }); }); it('passes right props to currency text', () => { - expect(orderSummaryTotal.find('[data-test="cart-price-item-total-note"]').find(TranslatedString).props()) - .toMatchObject({ - id: 'cart.billed_amount_text', - data: { - code: 'EUR', - total: '$100.00', - }, - }); + expect( + orderSummaryTotal + .find('[data-test="cart-price-item-total-note"]') + .find(TranslatedString) + .props(), + ).toMatchObject({ + id: 'cart.billed_amount_text', + data: { + code: 'EUR', + total: '$100.00', + }, + }); }); it('displays estimated total in shopper currency', () => { - expect(orderSummaryTotal.find('[data-test="cart-total"]').text()) - .toEqual('Estimated Total (USD) $112.00*'); + expect(orderSummaryTotal.find('[data-test="cart-total"]').text()).toBe( + 'Estimated Total (USD) $112.00*', + ); }); }); }); diff --git a/packages/core/src/app/order/OrderSummaryTotal.tsx b/packages/core/src/app/order/OrderSummaryTotal.tsx index cc88cc89c7..49ebe971d1 100644 --- a/packages/core/src/app/order/OrderSummaryTotal.tsx +++ b/packages/core/src/app/order/OrderSummaryTotal.tsx @@ -1,6 +1,6 @@ -import React, { Fragment, FunctionComponent } from 'react'; +import React, { FunctionComponent } from 'react'; -import { withCurrency, TranslatedString, WithCurrencyProps } from '../locale'; +import { TranslatedString, withCurrency, WithCurrencyProps } from '../locale'; import OrderSummaryPrice from './OrderSummaryPrice'; @@ -16,37 +16,39 @@ const OrderSummaryTotal: FunctionComponent { - const hasDifferentCurrency = shopperCurrencyCode !== storeCurrencyCode; - const label = - { hasDifferentCurrency ? - : - } - { ` (${shopperCurrencyCode})` } - ; + const label = ( + <> + {hasDifferentCurrency ? ( + + ) : ( + + )} + {` (${shopperCurrencyCode})`} + + ); return ( - + <> - { hasDifferentCurrency && currency &&

    - -

    } -
    + {hasDifferentCurrency && currency && ( +

    + +

    + )} + ); }; diff --git a/packages/core/src/app/order/PrintLink.tsx b/packages/core/src/app/order/PrintLink.tsx index bf62b9f9db..b6e0342f05 100644 --- a/packages/core/src/app/order/PrintLink.tsx +++ b/packages/core/src/app/order/PrintLink.tsx @@ -1,7 +1,7 @@ import { throttle } from 'lodash'; -import React, { memo, useCallback, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo, useCallback } from 'react'; -import { preventDefault } from "../common/dom"; +import { preventDefault } from '../common/dom'; import { TranslatedString } from '../locale'; import { IconPrint } from '../ui/icon'; @@ -12,10 +12,12 @@ export interface PrintLinkProps { const PRINT_MODAL_THROTTLE = 500; const PrintLink: FunctionComponent = ({ className }) => { - - const handleClick = useCallback(throttle(() => { - window.print(); - }, PRINT_MODAL_THROTTLE), []); + const handleClick = useCallback( + throttle(() => { + window.print(); + }, PRINT_MODAL_THROTTLE), + [], + ); if (typeof window.print !== 'function') { return null; @@ -23,14 +25,12 @@ const PrintLink: FunctionComponent = ({ className }) => { return ( - - { ' ' } - + ); }; diff --git a/packages/core/src/app/order/ThankYouHeader.tsx b/packages/core/src/app/order/ThankYouHeader.tsx index da7711f06d..a55e40c9e6 100644 --- a/packages/core/src/app/order/ThankYouHeader.tsx +++ b/packages/core/src/app/order/ThankYouHeader.tsx @@ -1,4 +1,4 @@ -import React, { memo, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo } from 'react'; import { TranslatedString } from '../locale'; import { PrimaryHeader } from '../ui/header'; @@ -7,18 +7,13 @@ export interface HeaderProps { name?: string; } -const ThankYouHeader: FunctionComponent = ({ - name, -}) => ( +const ThankYouHeader: FunctionComponent = ({ name }) => ( - { name && } + {name && ( + + )} - { !name && } + {!name && } ); diff --git a/packages/core/src/app/order/getItemsCount.spec.ts b/packages/core/src/app/order/getItemsCount.spec.ts index cb34fd5cac..8bfa5ccdb0 100644 --- a/packages/core/src/app/order/getItemsCount.spec.ts +++ b/packages/core/src/app/order/getItemsCount.spec.ts @@ -1,6 +1,16 @@ -import { CustomItem, DigitalItem, GiftCertificateItem, PhysicalItem } from '@bigcommerce/checkout-sdk'; +import { + CustomItem, + DigitalItem, + GiftCertificateItem, + PhysicalItem, +} from '@bigcommerce/checkout-sdk'; -import { getCustomItem, getDigitalItem, getGiftCertificateItem, getPhysicalItem } from '../cart/lineItem.mock'; +import { + getCustomItem, + getDigitalItem, + getGiftCertificateItem, + getPhysicalItem, +} from '../cart/lineItem.mock'; import getItemsCount from './getItemsCount'; @@ -16,8 +26,7 @@ describe('getItemsCount()', () => { }; it('returns zero', () => { - expect(getItemsCount(items)) - .toEqual(0); + expect(getItemsCount(items)).toBe(0); }); }); @@ -30,20 +39,13 @@ describe('getItemsCount()', () => { quantity: 2, }, ], - digitalItems: [ - getDigitalItem(), - ], - giftCertificates: [ - getGiftCertificateItem(), - ], - customItems: [ - getCustomItem(), - ], + digitalItems: [getDigitalItem()], + giftCertificates: [getGiftCertificateItem()], + customItems: [getCustomItem()], }; it('returns all quantities summed up', () => { - expect(getItemsCount(items)) - .toEqual(7); + expect(getItemsCount(items)).toBe(7); }); }); }); diff --git a/packages/core/src/app/order/getItemsCount.ts b/packages/core/src/app/order/getItemsCount.ts index 6cb3cb0737..826c4bbae8 100644 --- a/packages/core/src/app/order/getItemsCount.ts +++ b/packages/core/src/app/order/getItemsCount.ts @@ -6,11 +6,10 @@ export default function getItemsCount({ giftCertificates, customItems, }: LineItemMap): number { - const totalItemsCount = [ - ...physicalItems, - ...digitalItems, - ...(customItems || []), - ].reduce((total, item) => total += item.quantity, 0); + const totalItemsCount = [...physicalItems, ...digitalItems, ...(customItems || [])].reduce( + (total, item) => (total += item.quantity), + 0, + ); return totalItemsCount + giftCertificates.length; } diff --git a/packages/core/src/app/order/getLineItemsCount.ts b/packages/core/src/app/order/getLineItemsCount.ts index d2693f4c3c..13e7a692cd 100644 --- a/packages/core/src/app/order/getLineItemsCount.ts +++ b/packages/core/src/app/order/getLineItemsCount.ts @@ -6,8 +6,10 @@ export default function getLineItemsCount({ giftCertificates, customItems, }: LineItemMap): number { - return physicalItems.length + + return ( + physicalItems.length + digitalItems.length + giftCertificates.length + - (customItems || []).length; + (customItems || []).length + ); } diff --git a/packages/core/src/app/order/getOrderSummaryItemImage.tsx b/packages/core/src/app/order/getOrderSummaryItemImage.tsx index 1aa82345ec..dff608b3b7 100644 --- a/packages/core/src/app/order/getOrderSummaryItemImage.tsx +++ b/packages/core/src/app/order/getOrderSummaryItemImage.tsx @@ -6,9 +6,5 @@ export default function getOrderSummaryItemImage(item: DigitalItem | PhysicalIte return; } - return {; + return {item.name}; } diff --git a/packages/core/src/app/order/getPaymentInstructions.spec.ts b/packages/core/src/app/order/getPaymentInstructions.spec.ts index 0d3de43356..15d5f44bfd 100644 --- a/packages/core/src/app/order/getPaymentInstructions.spec.ts +++ b/packages/core/src/app/order/getPaymentInstructions.spec.ts @@ -8,20 +8,18 @@ describe('getPaymentInstructions()', () => { const order = getOrder(); it('returns instructions', () => { - expect(getPaymentInstructions(order)).toEqual('295 something'); + expect(getPaymentInstructions(order)).toBe('295 something'); }); }); describe('when order has no payments with instructions', () => { const order = { ...getOrder(), - payments: [ - getGiftCertificateOrderPayment(), - ], + payments: [getGiftCertificateOrderPayment()], }; it('returns empty string', () => { - expect(getPaymentInstructions(order)).toEqual(''); + expect(getPaymentInstructions(order)).toBe(''); }); }); @@ -32,7 +30,7 @@ describe('getPaymentInstructions()', () => { }; it('returns empty string', () => { - expect(getPaymentInstructions(order)).toEqual(''); + expect(getPaymentInstructions(order)).toBe(''); }); }); }); diff --git a/packages/core/src/app/order/getStoreCreditAmount.spec.ts b/packages/core/src/app/order/getStoreCreditAmount.spec.ts index 91bfebbcac..ea998c9a0e 100644 --- a/packages/core/src/app/order/getStoreCreditAmount.spec.ts +++ b/packages/core/src/app/order/getStoreCreditAmount.spec.ts @@ -1,13 +1,17 @@ import getStoreCreditAmount from './getStoreCreditAmount'; -import { getGatewayOrderPayment, getGiftCertificateOrderPayment, getOrder, getStoreCreditPayment } from './orders.mock'; +import { + getGatewayOrderPayment, + getGiftCertificateOrderPayment, + getOrder, + getStoreCreditPayment, +} from './orders.mock'; describe('getStoreCreditAmount()', () => { describe('when there are no store credit payments', () => { const { payments } = getOrder(); it('returns zero', () => { - expect(getStoreCreditAmount(payments)) - .toEqual(0); + expect(getStoreCreditAmount(payments)).toBe(0); }); }); @@ -20,8 +24,7 @@ describe('getStoreCreditAmount()', () => { ]; it('returns sum of them', () => { - expect(getStoreCreditAmount(payments)) - .toEqual(120); + expect(getStoreCreditAmount(payments)).toBe(120); }); }); }); diff --git a/packages/core/src/app/order/getStoreCreditAmount.ts b/packages/core/src/app/order/getStoreCreditAmount.ts index 5c71e5b68f..4105fdebbd 100644 --- a/packages/core/src/app/order/getStoreCreditAmount.ts +++ b/packages/core/src/app/order/getStoreCreditAmount.ts @@ -3,7 +3,7 @@ import { OrderPayment } from '@bigcommerce/checkout-sdk'; import { isStoreCreditPayment } from '../payment/storeCredit'; export default function getStoreCreditAmount(payments?: OrderPayment[]): number { - return (payments || []).filter(isStoreCreditPayment).reduce((total, payment) => - total + payment.amount - , 0); + return (payments || []) + .filter(isStoreCreditPayment) + .reduce((total, payment) => total + payment.amount, 0); } diff --git a/packages/core/src/app/order/mapFromDigital.spec.tsx b/packages/core/src/app/order/mapFromDigital.spec.tsx index 5cc2864c75..50ab547363 100644 --- a/packages/core/src/app/order/mapFromDigital.spec.tsx +++ b/packages/core/src/app/order/mapFromDigital.spec.tsx @@ -9,11 +9,9 @@ describe('mapFromDigital()', () => { const { options, ...item } = getDigitalItem(); const { productOptions = [] } = mapFromDigital(item); - expect(productOptions[0].testId) - .toEqual('cart-item-digital-product-download'); + expect(productOptions[0].testId).toBe('cart-item-digital-product-download'); - expect(productOptions[0].content) - .toMatchSnapshot(); + expect(productOptions[0].content).toMatchSnapshot(); }); it('returns digital related properties when it has no download URL', () => { @@ -24,10 +22,8 @@ describe('mapFromDigital()', () => { }; const { productOptions = [] } = mapFromDigital(item); - expect(productOptions[0].testId) - .toEqual('cart-item-digital-product'); + expect(productOptions[0].testId).toBe('cart-item-digital-product'); - expect(productOptions[0].content) - .toMatchSnapshot(); + expect(productOptions[0].content).toMatchSnapshot(); }); }); diff --git a/packages/core/src/app/order/mapFromDigital.tsx b/packages/core/src/app/order/mapFromDigital.tsx index fb18677ab9..15eb13c513 100644 --- a/packages/core/src/app/order/mapFromDigital.tsx +++ b/packages/core/src/app/order/mapFromDigital.tsx @@ -15,7 +15,7 @@ function mapFromDigital(item: DigitalItem): OrderSummaryItemProps { name: item.name, image: getOrderSummaryItemImage(item), productOptions: [ - ...(item.options || []).map(option => ({ + ...(item.options || []).map((option) => ({ testId: 'cart-item-product-option', content: `${option.name} ${option.value}`, })), @@ -34,14 +34,11 @@ function getDigitalItemDescription(item: DigitalItem): OrderSummaryItemOption { return { testId: 'cart-item-digital-product-download', - content: - + content: ( + - , + + ), }; } diff --git a/packages/core/src/app/order/mapFromGiftCertificate.tsx b/packages/core/src/app/order/mapFromGiftCertificate.tsx index fb2d72d325..24ca1b2691 100644 --- a/packages/core/src/app/order/mapFromGiftCertificate.tsx +++ b/packages/core/src/app/order/mapFromGiftCertificate.tsx @@ -11,13 +11,11 @@ function mapFromGiftCertificate(item: GiftCertificateItem): OrderSummaryItemProp quantity: 1, amount: item.amount, name: item.name, - image: - + image: ( + - , + + ), }; } diff --git a/packages/core/src/app/order/mapFromPhysical.tsx b/packages/core/src/app/order/mapFromPhysical.tsx index 7f6a142ceb..ad2d0e8f5a 100644 --- a/packages/core/src/app/order/mapFromPhysical.tsx +++ b/packages/core/src/app/order/mapFromPhysical.tsx @@ -12,7 +12,7 @@ function mapFromPhysical(item: PhysicalItem): OrderSummaryItemProps { name: item.name, image: getOrderSummaryItemImage(item), description: item.giftWrapping ? item.giftWrapping.name : undefined, - productOptions: (item.options || []).map(option => ({ + productOptions: (item.options || []).map((option) => ({ testId: 'cart-item-product-option', content: `${option.name} ${option.value}`, })), diff --git a/packages/core/src/app/order/orders.mock.ts b/packages/core/src/app/order/orders.mock.ts index c8203cb5b5..a2038b642c 100644 --- a/packages/core/src/app/order/orders.mock.ts +++ b/packages/core/src/app/order/orders.mock.ts @@ -1,4 +1,9 @@ -import { GatewayOrderPayment, GiftCertificateOrderPayment, Order, OrderPayment } from '@bigcommerce/checkout-sdk'; +import { + GatewayOrderPayment, + GiftCertificateOrderPayment, + Order, + OrderPayment, +} from '@bigcommerce/checkout-sdk'; import { getBillingAddress } from '../billing/billingAddresses.mock'; import { getGiftCertificateItem, getPhysicalItem } from '../cart/lineItem.mock'; @@ -10,10 +15,7 @@ export function getOrder(): Order { baseAmount: 200, billingAddress: getBillingAddress(), cartId: 'b20deef40f9699e48671bbc3fef6ca44dc80e3c7', - coupons: [ - getCoupon(), - getShippingCoupon(), - ], + coupons: [getCoupon(), getShippingCoupon()], currency: getCurrency(), customerMessage: '', customerCanBeCreated: true, @@ -25,13 +27,9 @@ export function getOrder(): Order { isDownloadable: false, isTaxIncluded: false, lineItems: { - physicalItems: [ - getPhysicalItem(), - ], + physicalItems: [getPhysicalItem()], digitalItems: [], - giftCertificates: [ - getGiftCertificateItem(), - ], + giftCertificates: [getGiftCertificateItem()], customItems: [], }, taxes: [ @@ -48,29 +46,22 @@ export function getOrder(): Order { orderAmount: 190, orderAmountAsInteger: 19000, orderId: 295, - payments: [ - getGatewayOrderPayment(), - getGiftCertificateOrderPayment(), - ], + payments: [getGatewayOrderPayment(), getGiftCertificateOrderPayment()], }; } export function getOrderWithMandateId(): Order { const order = getOrder(); - order.payments = [ - getGatewayOrderPaymentWithMandateId(), - getGiftCertificateOrderPayment(), - ]; + + order.payments = [getGatewayOrderPaymentWithMandateId(), getGiftCertificateOrderPayment()]; return order; } export function getOrderWithMandateURL(): Order { const order = getOrder(); - order.payments = [ - getGatewayOrderPaymentWithMandateURL(), - getGiftCertificateOrderPayment(), - ]; + + order.payments = [getGatewayOrderPaymentWithMandateURL(), getGiftCertificateOrderPayment()]; return order; } diff --git a/packages/core/src/app/order/removeBundledItems.spec.ts b/packages/core/src/app/order/removeBundledItems.spec.ts index 5b5e39847b..f32cb46378 100644 --- a/packages/core/src/app/order/removeBundledItems.spec.ts +++ b/packages/core/src/app/order/removeBundledItems.spec.ts @@ -1,4 +1,9 @@ -import { CustomItem, DigitalItem, GiftCertificateItem, PhysicalItem } from '@bigcommerce/checkout-sdk'; +import { + CustomItem, + DigitalItem, + GiftCertificateItem, + PhysicalItem, +} from '@bigcommerce/checkout-sdk'; import { getPhysicalItem } from '../cart/lineItem.mock'; @@ -15,8 +20,7 @@ describe('removeBundledItems()', () => { }; it('returns zero', () => { - expect(removeBundledItems(items)) - .toEqual(items); + expect(removeBundledItems(items)).toEqual(items); }); }); @@ -43,16 +47,13 @@ describe('removeBundledItems()', () => { }; it('removes items with parentId', () => { - expect(removeBundledItems(items)) - .toEqual(emptyItems); + expect(removeBundledItems(items)).toEqual(emptyItems); }); }); describe('when there are items without parentId', () => { const items = { - physicalItems: [ - getPhysicalItem(), - ], + physicalItems: [getPhysicalItem()], digitalItems: [] as DigitalItem[], giftCertificates: [] as GiftCertificateItem[], // for now, force it to be null this way. @@ -60,8 +61,7 @@ describe('removeBundledItems()', () => { }; it('keeps all items', () => { - expect(removeBundledItems(items)) - .toEqual(items); + expect(removeBundledItems(items)).toEqual(items); }); }); }); diff --git a/packages/core/src/app/order/removeBundledItems.ts b/packages/core/src/app/order/removeBundledItems.ts index 343ec4415b..53bf205679 100644 --- a/packages/core/src/app/order/removeBundledItems.ts +++ b/packages/core/src/app/order/removeBundledItems.ts @@ -3,7 +3,7 @@ import { LineItemMap } from '@bigcommerce/checkout-sdk'; export default function removeBundledItems(lineItems: LineItemMap): LineItemMap { return { ...lineItems, - physicalItems: lineItems.physicalItems.filter(item => (typeof item.parentId !== 'string')), - digitalItems: lineItems.digitalItems.filter(item => (typeof item.parentId !== 'string')), + physicalItems: lineItems.physicalItems.filter((item) => typeof item.parentId !== 'string'), + digitalItems: lineItems.digitalItems.filter((item) => typeof item.parentId !== 'string'), }; } diff --git a/packages/core/src/app/order/renderOrderConfirmation.spec.tsx b/packages/core/src/app/order/renderOrderConfirmation.spec.tsx index 610f298da5..b9fbc74791 100644 --- a/packages/core/src/app/order/renderOrderConfirmation.spec.tsx +++ b/packages/core/src/app/order/renderOrderConfirmation.spec.tsx @@ -1,7 +1,7 @@ import React, { FunctionComponent } from 'react'; -import renderOrderConfirmation, { RenderOrderConfirmationOptions } from './renderOrderConfirmation'; import { OrderConfirmationAppProps } from './OrderConfirmationApp'; +import renderOrderConfirmation, { RenderOrderConfirmationOptions } from './renderOrderConfirmation'; let OrderConfirmationApp: FunctionComponent; let configurePublicPath: (path: string) => void; @@ -10,7 +10,7 @@ let publicPath: string; jest.mock('@welldone-software/why-did-you-render', () => jest.fn()); jest.mock('../common/bundler', () => { - configurePublicPath = jest.fn(path => { + configurePublicPath = jest.fn((path) => { publicPath = path; return publicPath; @@ -22,7 +22,7 @@ jest.mock('../common/bundler', () => { }); jest.mock('./OrderConfirmationApp', () => { - OrderConfirmationApp = jest.fn(() => <>{ publicPath }); + OrderConfirmationApp = jest.fn(() => <>{publicPath}); return { default: OrderConfirmationApp, @@ -55,25 +55,21 @@ describe('renderOrderConfirmation()', () => { it('configures public path before mounting app component', () => { renderOrderConfirmation(options); - expect(configurePublicPath) - .toHaveBeenCalledWith(options.publicPath); + expect(configurePublicPath).toHaveBeenCalledWith(options.publicPath); - expect(container.innerHTML) - .toEqual(options.publicPath); + expect(container.innerHTML).toEqual(options.publicPath); }); it('passes props to app component', () => { renderOrderConfirmation(options); - expect(OrderConfirmationApp) - .toHaveBeenCalledWith(options, {}); + expect(OrderConfirmationApp).toHaveBeenCalledWith(options, {}); }); it('does not configure `whyDidYouRender` if not in development mode', () => { renderOrderConfirmation(options); - expect(require('@welldone-software/why-did-you-render')) - .not.toHaveBeenCalled(); + expect(require('@welldone-software/why-did-you-render')).not.toHaveBeenCalled(); process.env.NODE_ENV = 'development'; }); @@ -85,10 +81,9 @@ describe('renderOrderConfirmation()', () => { renderOrderConfirmation(options); - expect(require('@welldone-software/why-did-you-render')) - .toHaveBeenCalledWith(React, { - collapseGroups: true, - }); + expect(require('@welldone-software/why-did-you-render')).toHaveBeenCalledWith(React, { + collapseGroups: true, + }); process.env.NODE_ENV = env; }); diff --git a/packages/core/src/app/order/renderOrderConfirmation.tsx b/packages/core/src/app/order/renderOrderConfirmation.tsx index 4a57e78b99..3a818779ee 100644 --- a/packages/core/src/app/order/renderOrderConfirmation.tsx +++ b/packages/core/src/app/order/renderOrderConfirmation.tsx @@ -31,10 +31,10 @@ export default function renderOrderConfirmation({ ReactDOM.render( , - document.getElementById(containerId) + document.getElementById(containerId), ); } diff --git a/packages/core/src/app/orderComments/OrderComments.tsx b/packages/core/src/app/orderComments/OrderComments.tsx index 80b3f29339..6c9e9331a4 100644 --- a/packages/core/src/app/orderComments/OrderComments.tsx +++ b/packages/core/src/app/orderComments/OrderComments.tsx @@ -1,37 +1,38 @@ import { FieldProps } from 'formik'; -import React, { useCallback, useMemo, FunctionComponent } from 'react'; +import React, { FunctionComponent, useCallback, useMemo } from 'react'; import { TranslatedString } from '../locale'; import { Fieldset, FormField, Label, Legend, TextInput } from '../ui/form'; const OrderComments: FunctionComponent = () => { - const renderLabel = useCallback(name => ( - - ), []); + const renderLabel = useCallback( + (name) => ( + + ), + [], + ); - const renderInput = useCallback(({ field }: FieldProps) => ( - - ), []); + const renderInput = useCallback( + ({ field }: FieldProps) => , + [], + ); - const legend = useMemo(() => ( - - - - ), []); + const legend = useMemo( + () => ( + + + + ), + [], + ); - return
    - -
    ; + return ( +
    + +
    + ); }; export default OrderComments; diff --git a/packages/core/src/app/payment/Payment.spec.tsx b/packages/core/src/app/payment/Payment.spec.tsx index 7c993d412d..4ae4f71021 100644 --- a/packages/core/src/app/payment/Payment.spec.tsx +++ b/packages/core/src/app/payment/Payment.spec.tsx @@ -1,4 +1,11 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService, CustomError, PaymentMethod, RequestError } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, + CustomError, + PaymentMethod, + RequestError, +} from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import { EventEmitter } from 'events'; import { find, merge, noop } from 'lodash'; @@ -15,10 +22,10 @@ import { getOrder } from '../order/orders.mock'; import { getConsignment } from '../shipping/consignment.mock'; import { Button } from '../ui/button'; -import { PaymentMethodId } from './paymentMethod'; -import { getPaymentMethod } from './payment-methods.mock'; import Payment, { PaymentProps } from './Payment'; +import { getPaymentMethod } from './payment-methods.mock'; import PaymentForm, { PaymentFormProps } from './PaymentForm'; +import { PaymentMethodId } from './paymentMethod'; describe('Payment', () => { let PaymentTest: FunctionComponent; @@ -36,51 +43,45 @@ describe('Payment', () => { paymentMethods = [ getPaymentMethod(), { ...getPaymentMethod(), id: 'sagepay' }, - { ...getPaymentMethod(), id: 'bolt', initializationData: { showInCheckout: true }}, + { ...getPaymentMethod(), id: 'bolt', initializationData: { showInCheckout: true } }, ]; selectedPaymentMethod = paymentMethods[0]; subscribeEventEmitter = new EventEmitter(); - jest.spyOn(checkoutService, 'getState') - .mockImplementation(() => checkoutState); + jest.spyOn(checkoutService, 'getState').mockImplementation(() => checkoutState); - jest.spyOn(checkoutService, 'subscribe') - .mockImplementation(subscriber => { - subscribeEventEmitter.on('change', () => subscriber(checkoutService.getState())); - subscribeEventEmitter.emit('change'); + jest.spyOn(checkoutService, 'subscribe').mockImplementation((subscriber) => { + subscribeEventEmitter.on('change', () => subscriber(checkoutService.getState())); + subscribeEventEmitter.emit('change'); - return noop; - }); + return noop; + }); - jest.spyOn(checkoutService, 'loadPaymentMethods') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'loadPaymentMethods').mockResolvedValue(checkoutState); - jest.spyOn(checkoutService, 'finalizeOrderIfNeeded') - .mockRejectedValue({ type: 'order_finalization_not_required' }); + jest.spyOn(checkoutService, 'finalizeOrderIfNeeded').mockRejectedValue({ + type: 'order_finalization_not_required', + }); - jest.spyOn(checkoutState.data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(checkoutState.data, 'getCart').mockReturnValue(getCart()); - jest.spyOn(checkoutState.data, 'getCheckout') - .mockReturnValue(getCheckout()); + jest.spyOn(checkoutState.data, 'getCheckout').mockReturnValue(getCheckout()); - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - jest.spyOn(checkoutState.data, 'getCustomer') - .mockReturnValue(getCustomer()); + jest.spyOn(checkoutState.data, 'getCustomer').mockReturnValue(getCustomer()); - jest.spyOn(checkoutState.data, 'getOrder') - .mockReturnValue(merge(getOrder(), { isComplete: false })); + jest.spyOn(checkoutState.data, 'getOrder').mockReturnValue( + merge(getOrder(), { isComplete: false }), + ); - jest.spyOn(checkoutState.data, 'getPaymentMethods') - .mockReturnValue(paymentMethods); + jest.spyOn(checkoutState.data, 'getPaymentMethods').mockReturnValue(paymentMethods); - jest.spyOn(checkoutState.data, 'getPaymentMethod') - .mockImplementation(id => find(checkoutState.data.getPaymentMethods(), { id })); + jest.spyOn(checkoutState.data, 'getPaymentMethod').mockImplementation((id) => + find(checkoutState.data.getPaymentMethods(), { id }), + ); - jest.spyOn(checkoutState.data, 'isPaymentDataRequired') - .mockReturnValue(true); + jest.spyOn(checkoutState.data, 'isPaymentDataRequired').mockReturnValue(true); localeContext = createLocaleContext(getStoreConfig()); @@ -91,412 +92,383 @@ describe('Payment', () => { onUnhandledError: jest.fn(), }; - PaymentTest = props => ( - - - + PaymentTest = (props) => ( + + + ); }); it('renders payment form with expected props', async () => { - const container = mount(); + const container = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); - expect(container.find(PaymentForm).props()) - .toEqual(expect.objectContaining({ + expect(container.find(PaymentForm).props()).toEqual( + expect.objectContaining({ methods: paymentMethods, onSubmit: expect.any(Function), selectedMethod: paymentMethods[0], - })); + }), + ); }); it('does not render amazon if multi-shipping', async () => { paymentMethods.push({ ...getPaymentMethod(), id: 'amazonpay' }); - jest.spyOn(checkoutState.data, 'getConsignments') - .mockReturnValue([ - getConsignment(), - getConsignment(), - ]); + jest.spyOn(checkoutState.data, 'getConsignments').mockReturnValue([ + getConsignment(), + getConsignment(), + ]); - const container = mount(); + const container = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); paymentMethods.pop(); - expect(container.find(PaymentForm).prop('methods')) - .toEqual(paymentMethods); + expect(container.find(PaymentForm).prop('methods')).toEqual(paymentMethods); }); it('does not render bolt if showInCheckout is false', async () => { - const expectedPaymentMethods = paymentMethods.filter(method => method.id !== PaymentMethodId.Bolt); - paymentMethods[2] = { ...getPaymentMethod(), id: 'bolt', initializationData: { showInCheckout: false } }; + const expectedPaymentMethods = paymentMethods.filter( + (method) => method.id !== PaymentMethodId.Bolt, + ); + + paymentMethods[2] = { + ...getPaymentMethod(), + id: 'bolt', + initializationData: { showInCheckout: false }, + }; - const container = mount(); + const container = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); - expect(container.find(PaymentForm).props()) - .toEqual(expect.objectContaining({ + expect(container.find(PaymentForm).props()).toEqual( + expect.objectContaining({ methods: expectedPaymentMethods, onSubmit: expect.any(Function), selectedMethod: expectedPaymentMethods[0], - })); + }), + ); }); it('passes initialisation status to payment form', async () => { - jest.spyOn(checkoutState.statuses, 'isInitializingPayment') - .mockReturnValue(true); + jest.spyOn(checkoutState.statuses, 'isInitializingPayment').mockReturnValue(true); - const container = mount(); + const container = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); - expect(container.find(PaymentForm).prop('isInitializingPayment')) - .toEqual(true); + expect(container.find(PaymentForm).prop('isInitializingPayment')).toBe(true); }); it('does not render payment form until initial requests are made', async () => { - const container = mount(); + const container = mount(); - expect(container.find(PaymentForm).length) - .toEqual(0); + expect(container.find(PaymentForm)).toHaveLength(0); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); - expect(container.find(PaymentForm).length) - .toEqual(1); - expect(checkoutService.loadPaymentMethods) - .toHaveBeenCalled(); - expect(checkoutService.finalizeOrderIfNeeded) - .toHaveBeenCalled(); + expect(container.find(PaymentForm)).toHaveLength(1); + expect(checkoutService.loadPaymentMethods).toHaveBeenCalled(); + expect(checkoutService.finalizeOrderIfNeeded).toHaveBeenCalled(); }); it('does not render payment form if there are no methods', () => { - jest.spyOn(checkoutState.data, 'getPaymentMethods') - .mockReturnValue([]); + jest.spyOn(checkoutState.data, 'getPaymentMethods').mockReturnValue([]); - const container = mount(); + const container = mount(); - expect(container.find(PaymentForm).length) - .toEqual(0); + expect(container.find(PaymentForm)).toHaveLength(0); }); it('does not render payment form if the current order is complete', () => { - jest.spyOn(checkoutState.data, 'getOrder') - .mockReturnValue(getOrder()); + jest.spyOn(checkoutState.data, 'getOrder').mockReturnValue(getOrder()); - const container = mount(); + const container = mount(); - expect(container.find(PaymentForm).length) - .toEqual(0); + expect(container.find(PaymentForm)).toHaveLength(0); }); it('loads payment methods when component is mounted', async () => { - mount(); + mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(checkoutService.loadPaymentMethods) - .toHaveBeenCalled(); + expect(checkoutService.loadPaymentMethods).toHaveBeenCalled(); }); it('triggers callback when payment methods are loaded', async () => { const handeReady = jest.fn(); - mount(); + mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(handeReady) - .toHaveBeenCalled(); + expect(handeReady).toHaveBeenCalled(); }); it('calls applyStoreCredit when checkbox is clicked', async () => { - jest.spyOn(checkoutState.data, 'getCustomer') - .mockReturnValue({ - ...getCustomer(), - storeCredit: 10, - }); + jest.spyOn(checkoutState.data, 'getCustomer').mockReturnValue({ + ...getCustomer(), + storeCredit: 10, + }); - jest.spyOn(checkoutService, 'applyStoreCredit') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'applyStoreCredit').mockResolvedValue(checkoutState); - const component = mount(); + const component = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - component.find('input[name="useStoreCredit"]') + component + .find('input[name="useStoreCredit"]') .simulate('change', { target: { checked: false, name: 'useStoreCredit' } }); - expect(checkoutService.applyStoreCredit) - .toHaveBeenCalledWith(false); + expect(checkoutService.applyStoreCredit).toHaveBeenCalledWith(false); }); it('calls handleStoreCreditChange when component did mount', async () => { - jest.spyOn(checkoutService, 'applyStoreCredit') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'applyStoreCredit').mockResolvedValue(checkoutState); + const defaultProps = { errorLogger: createErrorLogger(), onSubmit: jest.fn(), onSubmitError: jest.fn(), onUnhandledError: jest.fn(), usableStoreCredit: 10, - } - mount(); + }; + + mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(checkoutService.applyStoreCredit) - .toHaveBeenCalledWith(true); + expect(checkoutService.applyStoreCredit).toHaveBeenCalledWith(true); }); it('sets default selected payment method', async () => { - const container = mount(); + const container = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); - expect(container.find(PaymentForm).prop('selectedMethod')) - .toEqual(paymentMethods[0]); + expect(container.find(PaymentForm).prop('selectedMethod')).toEqual(paymentMethods[0]); }); it('sets default selected payment method to the one with default stored instrument', async () => { paymentMethods[1].config.hasDefaultStoredInstrument = true; - const container = mount(); + const container = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); - expect(container.find(PaymentForm).props()) - .toEqual(expect.objectContaining({ + expect(container.find(PaymentForm).props()).toEqual( + expect.objectContaining({ methods: paymentMethods, selectedMethod: paymentMethods[1], - })); + }), + ); }); it('renders PaymentForm with usableStoreCredit=0 when grandTotal=0', async () => { - jest.spyOn(checkoutState.data, 'getCheckout') - .mockReturnValue({ - ...getCheckout(), - grandTotal: 0, - }); + jest.spyOn(checkoutState.data, 'getCheckout').mockReturnValue({ + ...getCheckout(), + grandTotal: 0, + }); - const container = mount(); + const container = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); - expect(container.find(PaymentForm).prop('usableStoreCredit')) - .toEqual(0); + expect(container.find(PaymentForm).prop('usableStoreCredit')).toBe(0); }); it('renders PaymentForm with grandTotal as usableStoreCredit when usableStoreCredit > grandTotal', async () => { - jest.spyOn(checkoutState.data, 'getCheckout') - .mockReturnValue({ - ...getCheckout(), - grandTotal: 20, - }); + jest.spyOn(checkoutState.data, 'getCheckout').mockReturnValue({ + ...getCheckout(), + grandTotal: 20, + }); - jest.spyOn(checkoutState.data, 'getCustomer') - .mockReturnValue({ - ...getCustomer(), - storeCredit: 100, - }); + jest.spyOn(checkoutState.data, 'getCustomer').mockReturnValue({ + ...getCustomer(), + storeCredit: 100, + }); - const container = mount(); + const container = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); - expect(container.find(PaymentForm).prop('usableStoreCredit')) - .toEqual(20); + expect(container.find(PaymentForm).prop('usableStoreCredit')).toBe(20); }); it('sets selected hosted payment as default selected payment method and filters methods prop to the hosted payment only', async () => { - jest.spyOn(checkoutState.data, 'getCheckout') - .mockReturnValue({ - ...getCheckout(), - payments: [{ + jest.spyOn(checkoutState.data, 'getCheckout').mockReturnValue({ + ...getCheckout(), + payments: [ + { ...getCheckoutPayment(), providerId: paymentMethods[1].id, - }], - }); + }, + ], + }); - const container = mount(); + const container = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); - expect(container.find(PaymentForm).props()) - .toEqual(expect.objectContaining({ + expect(container.find(PaymentForm).props()).toEqual( + expect.objectContaining({ methods: [paymentMethods[1]], selectedMethod: paymentMethods[1], - })); + }), + ); }); it('updates default selected payment method when list changes', async () => { - const container = mount(); + const container = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); - expect(container.find(PaymentForm).prop('selectedMethod')) - .toEqual(paymentMethods[0]); + expect(container.find(PaymentForm).prop('selectedMethod')).toEqual(paymentMethods[0]); // Update the list of payment methods so that its order is reversed checkoutState = merge({}, checkoutState, { data: { - getPaymentMethods: jest.fn(() => ([paymentMethods[1], paymentMethods[0]])), + getPaymentMethods: jest.fn(() => [paymentMethods[1], paymentMethods[0]]), }, }); subscribeEventEmitter.emit('change'); container.update(); - expect(container.find(PaymentForm).prop('selectedMethod')) - .toEqual(paymentMethods[1]); + expect(container.find(PaymentForm).prop('selectedMethod')).toEqual(paymentMethods[1]); }); it('tries to finalize order when component is mounted', async () => { - mount(); + mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(checkoutService.finalizeOrderIfNeeded) - .toHaveBeenCalled(); + expect(checkoutService.finalizeOrderIfNeeded).toHaveBeenCalled(); }); it('triggers completion handler if order is finalized successfully', async () => { - jest.spyOn(checkoutService, 'finalizeOrderIfNeeded') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'finalizeOrderIfNeeded').mockResolvedValue(checkoutState); const handleFinalize = jest.fn(); - mount(); + mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(handleFinalize) - .toHaveBeenCalled(); + expect(handleFinalize).toHaveBeenCalled(); }); it('triggers error handler if unable to finalize', async () => { - jest.spyOn(checkoutService, 'finalizeOrderIfNeeded') - .mockRejectedValue({ type: 'unknown_error' }); + jest.spyOn(checkoutService, 'finalizeOrderIfNeeded').mockRejectedValue({ + type: 'unknown_error', + }); const handleFinalizeError = jest.fn(); - mount(); + mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(handleFinalizeError) - .toHaveBeenCalled(); + expect(handleFinalizeError).toHaveBeenCalled(); }); it('does not trigger error handler if finalization is not required', async () => { const handleFinalizeError = jest.fn(); - mount(); + mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(handleFinalizeError) - .not.toHaveBeenCalled(); + expect(handleFinalizeError).not.toHaveBeenCalled(); }); it('checks if available payment methods are supported in embedded mode', () => { const checkEmbeddedSupport = jest.fn(); - mount(); + mount(); - expect(checkEmbeddedSupport) - .toHaveBeenCalledWith(paymentMethods.map(({ id }) => id)); + expect(checkEmbeddedSupport).toHaveBeenCalledWith(paymentMethods.map(({ id }) => id)); }); it('renders error modal if there is error when submitting order', () => { - jest.spyOn(checkoutState.errors, 'getSubmitOrderError') - .mockReturnValue({ type: 'payment_method_invalid' } as CustomError); + jest.spyOn(checkoutState.errors, 'getSubmitOrderError').mockReturnValue({ + type: 'payment_method_invalid', + } as CustomError); - const container = mount(); + const container = mount(); - expect(container.find(ErrorModal)) - .toHaveLength(1); + expect(container.find(ErrorModal)).toHaveLength(1); }); it('does not render error modal when there is cancellation error', () => { - jest.spyOn(checkoutState.errors, 'getSubmitOrderError') - .mockReturnValue({ type: 'payment_cancelled' } as CustomError); + jest.spyOn(checkoutState.errors, 'getSubmitOrderError').mockReturnValue({ + type: 'payment_cancelled', + } as CustomError); - const container = mount(); + const container = mount(); - expect(container.find(ErrorModal)) - .toHaveLength(0); + expect(container.find(ErrorModal)).toHaveLength(0); }); it('does not render error modal when there is spam protection not completed error', () => { - jest.spyOn(checkoutState.errors, 'getSubmitOrderError') - .mockReturnValue({ type: 'spam_protection_not_completed' } as CustomError); + jest.spyOn(checkoutState.errors, 'getSubmitOrderError').mockReturnValue({ + type: 'spam_protection_not_completed', + } as CustomError); - const container = mount(); + const container = mount(); - expect(container.find(ErrorModal)) - .toHaveLength(0); + expect(container.find(ErrorModal)).toHaveLength(0); }); it('does not render error modal when there is invalid payment form error', () => { - jest.spyOn(checkoutState.errors, 'getSubmitOrderError') - .mockReturnValue({ type: 'payment_invalid_form' } as CustomError); + jest.spyOn(checkoutState.errors, 'getSubmitOrderError').mockReturnValue({ + type: 'payment_invalid_form', + } as CustomError); - const container = mount(); + const container = mount(); - expect(container.find(ErrorModal)) - .toHaveLength(0); + expect(container.find(ErrorModal)).toHaveLength(0); }); it('does not render error modal when there is invalid hosted form value error', () => { - jest.spyOn(checkoutState.errors, 'getSubmitOrderError') - .mockReturnValue({ type: 'invalid_hosted_form_value' } as CustomError); + jest.spyOn(checkoutState.errors, 'getSubmitOrderError').mockReturnValue({ + type: 'invalid_hosted_form_value', + } as CustomError); - const container = mount(); + const container = mount(); - expect(container.find(ErrorModal)) - .toHaveLength(0); + expect(container.find(ErrorModal)).toHaveLength(0); }); it('renders error modal if there is error when finalizing order', () => { - jest.spyOn(checkoutState.errors, 'getFinalizeOrderError') - .mockReturnValue({ type: 'request' } as CustomError); + jest.spyOn(checkoutState.errors, 'getFinalizeOrderError').mockReturnValue({ + type: 'request', + } as CustomError); - const container = mount(); + const container = mount(); - expect(container.find(ErrorModal)) - .toHaveLength(1); + expect(container.find(ErrorModal)).toHaveLength(1); }); it('redirects to location error header when error type is provider_error', () => { @@ -509,14 +481,13 @@ describe('Payment', () => { writable: true, }); - jest.spyOn(checkoutState.errors, 'getFinalizeOrderError') - .mockReturnValue({ - type: 'request', - body: { type: 'provider_error' }, - headers: { location: 'foo' }, - } as unknown as RequestError); + jest.spyOn(checkoutState.errors, 'getFinalizeOrderError').mockReturnValue({ + type: 'request', + body: { type: 'provider_error' }, + headers: { location: 'foo' }, + } as unknown as RequestError); - const container = mount(); + const container = mount(); container.find(Button).simulate('click'); @@ -524,19 +495,19 @@ describe('Payment', () => { }); it('does not render error modal if order does not need to finalize', () => { - jest.spyOn(checkoutState.errors, 'getFinalizeOrderError') - .mockReturnValue({ type: 'order_finalization_not_required' } as CustomError); + jest.spyOn(checkoutState.errors, 'getFinalizeOrderError').mockReturnValue({ + type: 'order_finalization_not_required', + } as CustomError); - const container = mount(); + const container = mount(); - expect(container.find(ErrorModal)) - .toHaveLength(0); + expect(container.find(ErrorModal)).toHaveLength(0); }); it('passes validation schema to payment form', async () => { - const container = mount(); + const container = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); const form: ReactWrapper = container.find(PaymentForm); @@ -545,24 +516,20 @@ describe('Payment', () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion form.prop('validationSchema')!.validateSync({ ccNumber: '' }); } catch (error) { - expect(error.name) - .toEqual('ValidationError'); + expect(error.name).toBe('ValidationError'); } }); it('submits order with payment data if payment is required', async () => { - jest.spyOn(checkoutState.data, 'isPaymentDataRequired') - .mockReturnValue(true); + jest.spyOn(checkoutState.data, 'isPaymentDataRequired').mockReturnValue(true); - jest.spyOn(checkoutState.data, 'isPaymentDataSubmitted') - .mockReturnValue(false); + jest.spyOn(checkoutState.data, 'isPaymentDataSubmitted').mockReturnValue(false); - jest.spyOn(checkoutService, 'submitOrder') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'submitOrder').mockResolvedValue(checkoutState); - const container = mount(); + const container = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); const form: ReactWrapper = container.find(PaymentForm); @@ -576,31 +543,29 @@ describe('Payment', () => { paymentProviderRadio: selectedPaymentMethod.id, }); - expect(checkoutService.submitOrder) - .toHaveBeenCalledWith({ - payment: { - methodId: selectedPaymentMethod.id, - gatewayId: selectedPaymentMethod.gateway, - paymentData: { - ccCvv: '123', - ccExpiry: { - month: '10', - year: '2025', - }, - ccName: 'test', - ccNumber: '4111111111111111', + expect(checkoutService.submitOrder).toHaveBeenCalledWith({ + payment: { + methodId: selectedPaymentMethod.id, + gatewayId: selectedPaymentMethod.gateway, + paymentData: { + ccCvv: '123', + ccExpiry: { + month: '10', + year: '2025', }, + ccName: 'test', + ccNumber: '4111111111111111', }, - }); + }, + }); }); it('triggers callback when order is submitted successfully', async () => { - jest.spyOn(checkoutService, 'submitOrder') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'submitOrder').mockResolvedValue(checkoutState); - const container = mount(); + const container = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); const form: ReactWrapper = container.find(PaymentForm); @@ -614,19 +579,17 @@ describe('Payment', () => { paymentProviderRadio: selectedPaymentMethod.id, }); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(defaultProps.onSubmit) - .toHaveBeenCalled(); + expect(defaultProps.onSubmit).toHaveBeenCalled(); }); it('triggers error callback when order fails to submit', async () => { - jest.spyOn(checkoutService, 'submitOrder') - .mockRejectedValue(checkoutState); + jest.spyOn(checkoutService, 'submitOrder').mockRejectedValue(checkoutState); - const container = mount(); + const container = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); const form: ReactWrapper = container.find(PaymentForm); @@ -640,25 +603,21 @@ describe('Payment', () => { paymentProviderRadio: selectedPaymentMethod.id, }); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(defaultProps.onSubmitError) - .toHaveBeenCalled(); + expect(defaultProps.onSubmitError).toHaveBeenCalled(); }); it('submits order with selected instrument if payment is required', async () => { - jest.spyOn(checkoutState.data, 'isPaymentDataRequired') - .mockReturnValue(true); + jest.spyOn(checkoutState.data, 'isPaymentDataRequired').mockReturnValue(true); - jest.spyOn(checkoutState.data, 'isPaymentDataSubmitted') - .mockReturnValue(false); + jest.spyOn(checkoutState.data, 'isPaymentDataSubmitted').mockReturnValue(false); - jest.spyOn(checkoutService, 'submitOrder') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'submitOrder').mockResolvedValue(checkoutState); - const container = mount(); + const container = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); const form: ReactWrapper = container.find(PaymentForm); @@ -671,40 +630,35 @@ describe('Payment', () => { paymentProviderRadio: selectedPaymentMethod.id, }); - expect(checkoutService.submitOrder) - .toHaveBeenCalledWith({ - payment: { - methodId: selectedPaymentMethod.id, - gatewayId: selectedPaymentMethod.gateway, - paymentData: { - ccCvv: '123', - ccNumber: '4111111111111111', - instrumentId: '123', - }, + expect(checkoutService.submitOrder).toHaveBeenCalledWith({ + payment: { + methodId: selectedPaymentMethod.id, + gatewayId: selectedPaymentMethod.gateway, + paymentData: { + ccCvv: '123', + ccNumber: '4111111111111111', + instrumentId: '123', }, - }); + }, + }); }); it('reloads checkout object if unable to submit order due to spam protection error', async () => { - jest.spyOn(checkoutService, 'loadCheckout') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'loadCheckout').mockResolvedValue(checkoutState); - jest.spyOn(checkoutState.errors, 'getSubmitOrderError') - .mockReturnValue({ - type: 'request', - body: { type: 'spam_protection_expired' }, - } as unknown as RequestError); + jest.spyOn(checkoutState.errors, 'getSubmitOrderError').mockReturnValue({ + type: 'request', + body: { type: 'spam_protection_expired' }, + } as unknown as RequestError); - const container = mount(); + const container = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); container.find('ErrorModal Button').simulate('click'); - expect(checkoutService.loadCheckout) - .toHaveBeenCalled(); - expect(container.find(PaymentForm).prop('didExceedSpamLimit')) - .toBeTruthy(); + expect(checkoutService.loadCheckout).toHaveBeenCalled(); + expect(container.find(PaymentForm).prop('didExceedSpamLimit')).toBeTruthy(); }); }); diff --git a/packages/core/src/app/payment/Payment.tsx b/packages/core/src/app/payment/Payment.tsx index 06e36bec1d..e3cad034aa 100644 --- a/packages/core/src/app/payment/Payment.tsx +++ b/packages/core/src/app/payment/Payment.tsx @@ -1,12 +1,25 @@ -import { PaymentFormValues } from '@bigcommerce/checkout/payment-integration-api'; -import { CartChangedError, CheckoutSelectors, CheckoutSettings, OrderRequestBody, PaymentMethod } from '@bigcommerce/checkout-sdk'; +import { + CartChangedError, + CheckoutSelectors, + CheckoutSettings, + OrderRequestBody, + PaymentMethod, +} from '@bigcommerce/checkout-sdk'; import { memoizeOne } from '@bigcommerce/memoize'; import { compact, find, isEmpty, noop } from 'lodash'; import React, { Component, ReactNode } from 'react'; import { ObjectSchema } from 'yup'; -import { withCheckout, CheckoutContextProps } from '../checkout'; -import { isCartChangedError, ErrorModal, ErrorModalOnCloseProps, ErrorLogger, isErrorWithType } from '../common/error'; +import { PaymentFormValues } from '@bigcommerce/checkout/payment-integration-api'; + +import { CheckoutContextProps, withCheckout } from '../checkout'; +import { + ErrorLogger, + ErrorModal, + ErrorModalOnCloseProps, + isCartChangedError, + isErrorWithType, +} from '../common/error'; import { EMPTY_ARRAY } from '../common/utility'; import { withLanguage, WithLanguageProps } from '../locale'; import { TermsConditionsType } from '../termsConditions'; @@ -14,9 +27,13 @@ import { LoadingOverlay } from '../ui/loading'; import mapSubmitOrderErrorMessage, { mapSubmitOrderErrorTitle } from './mapSubmitOrderErrorMessage'; import mapToOrderRequestBody from './mapToOrderRequestBody'; -import { getUniquePaymentMethodId, PaymentMethodId, PaymentMethodProviderType } from './paymentMethod'; import PaymentContext from './PaymentContext'; import PaymentForm from './PaymentForm'; +import { + getUniquePaymentMethodId, + PaymentMethodId, + PaymentMethodProviderType, +} from './paymentMethod'; export interface PaymentProps { errorLogger: ErrorLogger; @@ -67,7 +84,10 @@ interface PaymentState { validationSchemas: { [key: string]: ObjectSchema> | null }; } -class Payment extends Component { +class Payment extends Component< + PaymentProps & WithCheckoutPaymentProps & WithLanguageProps, + PaymentState +> { state: PaymentState = { didExceedSpamLimit: false, isReady: false, @@ -110,6 +130,7 @@ class Payment extends Component id)); } @@ -155,98 +173,101 @@ class Payment extends Component - - { !isEmpty(methods) && defaultMethod && } + + + {!isEmpty(methods) && defaultMethod && ( + + )} - { this.renderOrderErrorModal() } - { this.renderEmbeddedSupportErrorModal() } + {this.renderOrderErrorModal()} + {this.renderEmbeddedSupportErrorModal()} ); } private renderOrderErrorModal(): ReactNode { - const { - finalizeOrderError, - language, - shouldLocaliseErrorMessages, - submitOrderError, - } = this.props; + const { finalizeOrderError, language, shouldLocaliseErrorMessages, submitOrderError } = + this.props; // FIXME: Export correct TS interface const error: any = submitOrderError || finalizeOrderError; - if (!error || + if ( + !error || error.type === 'order_finalization_not_required' || error.type === 'payment_cancelled' || error.type === 'payment_invalid_form' || error.type === 'spam_protection_not_completed' || - error.type === 'invalid_hosted_form_value') { + error.type === 'invalid_hosted_form_value' + ) { return null; } return ( ); } private renderEmbeddedSupportErrorModal(): ReactNode { - const { - checkEmbeddedSupport = noop, - methods, - } = this.props; + const { checkEmbeddedSupport = noop, methods } = this.props; try { checkEmbeddedSupport(methods.map(({ id }) => id)); } catch (error) { if (error instanceof Error) { - return ( - - ); + return ; } } return null; } - private disableSubmit: ( - method: PaymentMethod, - disabled?: boolean - ) => void = (method, disabled = true) => { + private disableSubmit: (method: PaymentMethod, disabled?: boolean) => void = ( + method, + disabled = true, + ) => { const uniqueId = getUniquePaymentMethodId(method.id, method.gateway); const { shouldDisableSubmit } = this.state; @@ -262,10 +283,10 @@ class Payment extends Component void = (method, disabled = true) => { + private hidePaymentSubmitButton: (method: PaymentMethod, disabled?: boolean) => void = ( + method, + disabled = true, + ) => { const uniqueId = getUniquePaymentMethodId(method.id, method.gateway); const { shouldHidePaymentSubmitButton } = this.state; @@ -282,7 +303,7 @@ class Payment extends Component string | undefined = event => { + private handleBeforeUnload: (event: BeforeUnloadEvent) => string | undefined = (event) => { const { defaultMethod, isSubmittingOrder, language } = this.props; const { selectedMethod = defaultMethod } = this.state; @@ -290,7 +311,8 @@ class Payment extends Component Promise = async (_, { error }) => { - if (!error) { - return; - } - - const { cartUrl, clearError, loadCheckout } = this.props; - const { type: errorType } = error as any; // FIXME: Export correct TS interface - - if (errorType === 'provider_fatal_error' || - errorType === 'order_could_not_be_finalized_error') { - window.location.replace(cartUrl || '/'); - } - - if (errorType === 'tax_provider_unavailable') { - window.location.reload(); - } + private handleCloseModal: (event: Event, props: ErrorModalOnCloseProps) => Promise = + async (_, { error }) => { + if (!error) { + return; + } - if (isErrorWithType(error)) { - const { body, headers, status } = error; + const { cartUrl, clearError, loadCheckout } = this.props; + const { type: errorType } = error as any; // FIXME: Export correct TS interface - if (body.type === 'provider_error' && headers.location) { - window?.top?.location.assign(headers.location); + if ( + errorType === 'provider_fatal_error' || + errorType === 'order_could_not_be_finalized_error' + ) { + window.location.replace(cartUrl || '/'); } - // Reload the checkout object to get the latest `shouldExecuteSpamCheck` value, - // which will in turn make `SpamProtectionField` visible again. - // NOTE: As a temporary fix, we're checking the status code instead of the error - // type because of an issue with Nginx config, which causes the server to return - // HTML page instead of JSON response when there is a 429 error. - if (status === 429 || body.type === 'spam_protection_expired' || body.type === 'spam_protection_failed') { - this.setState({ didExceedSpamLimit: true }); + if (errorType === 'tax_provider_unavailable') { + window.location.reload(); + } - await loadCheckout(); + if (isErrorWithType(error)) { + const { body, headers, status } = error; + + if (body.type === 'provider_error' && headers.location) { + window.top?.location.assign(headers.location); + } + + // Reload the checkout object to get the latest `shouldExecuteSpamCheck` value, + // which will in turn make `SpamProtectionField` visible again. + // NOTE: As a temporary fix, we're checking the status code instead of the error + // type because of an issue with Nginx config, which causes the server to return + // HTML page instead of JSON response when there is a 429 error. + if ( + status === 429 || + body.type === 'spam_protection_expired' || + body.type === 'spam_protection_failed' + ) { + this.setState({ didExceedSpamLimit: true }); + + await loadCheckout(); + } } - } - clearError(error); - }; + clearError(error); + }; - private handleStoreCreditChange: (useStoreCredit: boolean) => void = async useStoreCredit => { - const { - applyStoreCredit, - onUnhandledError = noop, - } = this.props; + private handleStoreCreditChange: (useStoreCredit: boolean) => void = async (useStoreCredit) => { + const { applyStoreCredit, onUnhandledError = noop } = this.props; try { await applyStoreCredit(useStoreCredit); @@ -383,10 +407,7 @@ class Payment extends Component void = (error: Error) => { - const { - onUnhandledError = noop, - errorLogger - } = this.props; + const { onUnhandledError = noop, errorLogger } = this.props; const { type } = error as any; @@ -395,11 +416,11 @@ class Payment extends Component void = async values => { + private handleSubmit: (values: PaymentFormValues) => void = async (values) => { const { defaultMethod, loadPaymentMethods, @@ -410,14 +431,11 @@ class Payment extends Component void = method => { + private setSelectedMethod: (method?: PaymentMethod) => void = (method) => { const { selectedMethod } = this.state; if (selectedMethod === method) { @@ -452,7 +471,7 @@ class Payment extends Component void | null + fn: (values: PaymentFormValues) => void | null, ) => void = (method, fn) => { const uniqueId = getUniquePaymentMethodId(method.id, method.gateway); const { submitFunctions } = this.state; @@ -471,7 +490,7 @@ class Payment extends Component> | null + schema: ObjectSchema> | null, ) => void = (method, schema) => { const uniqueId = getUniquePaymentMethodId(method.id, method.gateway); const { validationSchemas } = this.state; @@ -504,14 +523,8 @@ export function mapToPaymentProps({ getPaymentMethods, isPaymentDataRequired, }, - errors: { - getFinalizeOrderError, - getSubmitOrderError, - }, - statuses: { - isInitializingPayment, - isSubmittingOrder, - }, + errors: { getFinalizeOrderError, getSubmitOrderError }, + statuses: { isInitializingPayment, isSubmittingOrder }, } = checkoutState; const checkout = getCheckout(); @@ -534,7 +547,9 @@ export function mapToPaymentProps({ } = config.checkoutSettings as CheckoutSettings & { orderTermsAndConditionsLocation: string }; const isTermsConditionsRequired = isTermsConditionsEnabled; - const selectedPayment = find(checkout.payments, { providerType: PaymentMethodProviderType.Hosted }); + const selectedPayment = find(checkout.payments, { + providerType: PaymentMethodProviderType.Hosted, + }); const { isStoreCreditApplied } = checkout; @@ -561,10 +576,17 @@ export function mapToPaymentProps({ } if (selectedPayment) { - selectedPaymentMethod = getPaymentMethod(selectedPayment.providerId, selectedPayment.gatewayId); - filteredMethods = selectedPaymentMethod ? compact([selectedPaymentMethod]) : filteredMethods; + selectedPaymentMethod = getPaymentMethod( + selectedPayment.providerId, + selectedPayment.gatewayId, + ); + filteredMethods = selectedPaymentMethod + ? compact([selectedPaymentMethod]) + : filteredMethods; } else { - selectedPaymentMethod = find(filteredMethods, { config: { hasDefaultStoredInstrument: true } }); + selectedPaymentMethod = find(filteredMethods, { + config: { hasDefaultStoredInstrument: true }, + }); // eslint-disable-next-line no-self-assign filteredMethods = filteredMethods; } @@ -574,7 +596,7 @@ export function mapToPaymentProps({ availableStoreCredit: customer.storeCredit, cartUrl: config.links.cartLink, clearError: checkoutService.clearError, - defaultMethod: selectedPaymentMethod ? selectedPaymentMethod : filteredMethods[0], + defaultMethod: selectedPaymentMethod || filteredMethods[0], finalizeOrderError: getFinalizeOrderError(), finalizeOrderIfNeeded: checkoutService.finalizeOrderIfNeeded, loadCheckout: checkoutService.loadCheckout, @@ -586,17 +608,20 @@ export function mapToPaymentProps({ loadPaymentMethods: checkoutService.loadPaymentMethods, methods: filteredMethods, shouldExecuteSpamCheck: checkout.shouldExecuteSpamCheck, - shouldLocaliseErrorMessages: features['PAYMENTS-6799.localise_checkout_payment_error_messages'], + shouldLocaliseErrorMessages: + features['PAYMENTS-6799.localise_checkout_payment_error_messages'], submitOrder: checkoutService.submitOrder, submitOrderError: getSubmitOrderError(), - termsConditionsText: isTermsConditionsRequired && termsConditionsType === TermsConditionsType.TextArea ? - termsCondtitionsText : - undefined, - termsConditionsUrl: isTermsConditionsRequired && termsConditionsType === TermsConditionsType.Link ? - termsCondtitionsUrl : - undefined, - usableStoreCredit: checkout.grandTotal > 0 ? - Math.min(checkout.grandTotal, customer.storeCredit || 0) : 0, + termsConditionsText: + isTermsConditionsRequired && termsConditionsType === TermsConditionsType.TextArea + ? termsCondtitionsText + : undefined, + termsConditionsUrl: + isTermsConditionsRequired && termsConditionsType === TermsConditionsType.Link + ? termsCondtitionsUrl + : undefined, + usableStoreCredit: + checkout.grandTotal > 0 ? Math.min(checkout.grandTotal, customer.storeCredit || 0) : 0, }; } diff --git a/packages/core/src/app/payment/PaymentContext.tsx b/packages/core/src/app/payment/PaymentContext.tsx index a1ca84e135..1196ad6d50 100644 --- a/packages/core/src/app/payment/PaymentContext.tsx +++ b/packages/core/src/app/payment/PaymentContext.tsx @@ -1,8 +1,9 @@ -import { PaymentFormValues } from '@bigcommerce/checkout/payment-integration-api'; import { PaymentMethod } from '@bigcommerce/checkout-sdk'; import { createContext } from 'react'; import { ObjectSchema } from 'yup'; +import { PaymentFormValues } from '@bigcommerce/checkout/payment-integration-api'; + export interface PaymentContextProps { disableSubmit(method: PaymentMethod, disabled?: boolean): void; // NOTE: This prop allows certain payment methods to override the default @@ -13,7 +14,10 @@ export interface PaymentContextProps { // snowflake behaviours. In the future, if we decide to change the UX, we // can remove this prop. setSubmit(method: PaymentMethod, fn: ((values: PaymentFormValues) => void) | null): void; - setValidationSchema(method: PaymentMethod, schema: ObjectSchema> | null): void; + setValidationSchema( + method: PaymentMethod, + schema: ObjectSchema> | null, + ): void; hidePaymentSubmitButton(method: PaymentMethod, hidden?: boolean): void; } diff --git a/packages/core/src/app/payment/PaymentForm.spec.tsx b/packages/core/src/app/payment/PaymentForm.spec.tsx index e6408ec189..eabc423a41 100644 --- a/packages/core/src/app/payment/PaymentForm.spec.tsx +++ b/packages/core/src/app/payment/PaymentForm.spec.tsx @@ -1,4 +1,9 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService, PaymentMethod } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, + PaymentMethod, +} from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import React, { FunctionComponent } from 'react'; import { act } from 'react-dom/test-utils'; @@ -8,16 +13,20 @@ import { CheckoutProvider } from '../checkout'; import { getStoreConfig } from '../config/config.mock'; import { getCustomer } from '../customer/customers.mock'; import { createLocaleContext, LocaleContext, LocaleContextType } from '../locale'; -import { TermsConditionsField, TermsConditionsFieldProps, TermsConditionsType } from '../termsConditions'; +import { + TermsConditionsField, + TermsConditionsFieldProps, + TermsConditionsType, +} from '../termsConditions'; import { getCreditCardValidationSchema } from './creditCard'; import { getPaymentMethod } from './payment-methods.mock'; -import { PaymentMethodList, PaymentMethodListProps } from './paymentMethod'; -import { StoreCreditField, StoreCreditFieldProps, StoreCreditOverlay } from './storeCredit'; import PaymentContext, { PaymentContextProps } from './PaymentContext'; import PaymentForm, { PaymentFormProps } from './PaymentForm'; +import { PaymentMethodList, PaymentMethodListProps } from './paymentMethod'; import PaymentSubmitButton from './PaymentSubmitButton'; import SpamProtectionField, { SpamProtectionProps } from './SpamProtectionField'; +import { StoreCreditField, StoreCreditFieldProps, StoreCreditOverlay } from './storeCredit'; jest.useFakeTimers(); @@ -34,10 +43,7 @@ describe('PaymentForm', () => { isStoreCreditApplied: true, defaultMethodId: getPaymentMethod().id, isPaymentDataRequired: jest.fn(() => true), - methods: [ - getPaymentMethod(), - { ...getPaymentMethod(), id: 'cybersource' }, - ], + methods: [getPaymentMethod(), { ...getPaymentMethod(), id: 'cybersource' }], onSubmit: jest.fn(), }; @@ -51,23 +57,19 @@ describe('PaymentForm', () => { hidePaymentSubmitButton: jest.fn(), }; - jest.spyOn(checkoutService, 'initializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'initializePayment').mockResolvedValue(checkoutState); - jest.spyOn(checkoutState.data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(checkoutState.data, 'getCart').mockReturnValue(getCart()); - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - jest.spyOn(checkoutState.data, 'getCustomer') - .mockReturnValue(getCustomer()); + jest.spyOn(checkoutState.data, 'getCustomer').mockReturnValue(getCustomer()); - PaymentFormTest = props => ( - - - - + PaymentFormTest = (props) => ( + + + + @@ -75,233 +77,218 @@ describe('PaymentForm', () => { }); it('renders list of payment methods', () => { - const container = mount(); + const container = mount(); const methodList: ReactWrapper = container.find(PaymentMethodList); - expect(methodList) - .toHaveLength(1); + expect(methodList).toHaveLength(1); - expect(methodList.prop('methods')) - .toEqual(defaultProps.methods); + expect(methodList.prop('methods')).toEqual(defaultProps.methods); }); it('renders terms and conditions field if copy is provided', () => { - const container = mount(); - const termsField: ReactWrapper = container.find(TermsConditionsField); - - expect(termsField) - .toHaveLength(1); - - expect(termsField.props()) - .toEqual(expect.objectContaining({ + const container = mount( + , + ); + const termsField: ReactWrapper = + container.find(TermsConditionsField); + + expect(termsField).toHaveLength(1); + + expect(termsField.props()).toEqual( + expect.objectContaining({ terms: 'Accept terms', type: TermsConditionsType.TextArea, - })); + }), + ); }); it('renders terms and conditions field if terms URL is provided', () => { - const container = mount(); - const termsField: ReactWrapper = container.find(TermsConditionsField); - - expect(termsField) - .toHaveLength(1); - - expect(termsField.props()) - .toEqual(expect.objectContaining({ + const container = mount( + , + ); + const termsField: ReactWrapper = + container.find(TermsConditionsField); + + expect(termsField).toHaveLength(1); + + expect(termsField.props()).toEqual( + expect.objectContaining({ url: 'https://foobar.com/terms', type: TermsConditionsType.Link, - })); + }), + ); }); it('does not render terms and conditions field if it is not required', () => { - const container = mount(); + const container = mount(); - expect(container.find(TermsConditionsField)) - .toHaveLength(0); + expect(container.find(TermsConditionsField)).toHaveLength(0); }); it('renders spam protection field if spam check should be executed', () => { - const container = mount(); - const spamProtectionField: ReactWrapper = container.find(SpamProtectionField); - - expect(spamProtectionField) - .toHaveLength(1); + const container = mount( + , + ); + const spamProtectionField: ReactWrapper = + container.find(SpamProtectionField); + + expect(spamProtectionField).toHaveLength(1); }); it('renders store credit field if store credit can be applied', () => { - const container = mount(); - const storeCreditField: ReactWrapper = container.find(StoreCreditField); + const container = mount(); + const storeCreditField: ReactWrapper = + container.find(StoreCreditField); - expect(storeCreditField) - .toHaveLength(1); + expect(storeCreditField).toHaveLength(1); - expect(storeCreditField.props()) - .toEqual(expect.objectContaining({ + expect(storeCreditField.props()).toEqual( + expect.objectContaining({ name: 'useStoreCredit', usableStoreCredit: 100, - })); + }), + ); }); it('does not render store credit field if store credit cannot be applied', () => { - const container = mount(); + const container = mount(); - expect(container.find(StoreCreditField).exists()) - .toEqual(false); + expect(container.find(StoreCreditField).exists()).toBe(false); }); it('does not render store credit field if store credit cannot be applied', () => { - const container = mount(); + const container = mount(); - expect(container.find(StoreCreditField).prop('usableStoreCredit')) - .toEqual(10); + expect(container.find(StoreCreditField).prop('usableStoreCredit')).toBe(10); }); it('shows overlay if store credit can cover total cost of order', () => { - jest.spyOn(defaultProps, 'isPaymentDataRequired') - .mockReturnValue(false); + jest.spyOn(defaultProps, 'isPaymentDataRequired').mockReturnValue(false); - const container = mount(); + const container = mount(); - expect(container.find(StoreCreditOverlay)) - .toHaveLength(1); + expect(container.find(StoreCreditOverlay)).toHaveLength(1); }); it('does not show overlay if store credit cannot cover total cost of order', () => { - const container = mount(); + const container = mount(); - expect(container.find(StoreCreditOverlay)) - .toHaveLength(0); + expect(container.find(StoreCreditOverlay)).toHaveLength(0); }); it('notifies parent when user selects new payment method', () => { const handleSelect = jest.fn(); - const container = mount(); + const container = mount( + , + ); const methodList: ReactWrapper = container.find(PaymentMethodList); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion methodList.prop('onSelect')!(defaultProps.methods[0]); - expect(handleSelect) - .toHaveBeenCalled(); + expect(handleSelect).toHaveBeenCalled(); }); it('passes form values to parent component', async () => { const handleSubmit = jest.fn(); - const container = mount(); + const container = mount(); - container.find('input[name="ccNumber"]') + container + .find('input[name="ccNumber"]') .simulate('change', { target: { value: '4111 1111 1111 1111', name: 'ccNumber' } }); - container.find('input[name="ccCvv"]') + container + .find('input[name="ccCvv"]') .simulate('change', { target: { value: '123', name: 'ccCvv' } }); - container.find('input[name="ccName"]') + container + .find('input[name="ccName"]') .simulate('change', { target: { value: 'Foo Bar', name: 'ccName' } }); - container.find('input[name="ccExpiry"]') + container + .find('input[name="ccExpiry"]') .simulate('change', { target: { value: '10 / 22', name: 'ccExpiry' } }); - container.find('form') - .simulate('submit'); - - await new Promise(resolve => process.nextTick(resolve)); - - expect(handleSubmit) - .toHaveBeenCalledWith({ - ccNumber: '4111 1111 1111 1111', - ccCvv: '123', - ccName: 'Foo Bar', - ccExpiry: '10 / 22', - paymentProviderRadio: defaultProps.defaultMethodId, - shouldCreateAccount: true, - shouldSaveInstrument: false, - terms: false, - }); + container.find('form').simulate('submit'); + + await new Promise((resolve) => process.nextTick(resolve)); + + expect(handleSubmit).toHaveBeenCalledWith({ + ccNumber: '4111 1111 1111 1111', + ccCvv: '123', + ccName: 'Foo Bar', + ccExpiry: '10 / 22', + paymentProviderRadio: defaultProps.defaultMethodId, + shouldCreateAccount: true, + shouldSaveInstrument: false, + terms: false, + }); }); it('does not pass form values to parent component if validation fails', async () => { const handleSubmit = jest.fn(); - const container = mount(); - - container.find('input[name="ccNumber"]') + const container = mount( + , + ); + + container + .find('input[name="ccNumber"]') .simulate('change', { target: { value: '4111', name: 'ccNumber' } }); - container.find('input[name="ccExpiry"]') + container + .find('input[name="ccExpiry"]') .simulate('change', { target: { value: '10 / 22', name: 'ccExpiry' } }); - container.find('form') - .simulate('submit'); + container.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(handleSubmit) - .not.toHaveBeenCalled(); + expect(handleSubmit).not.toHaveBeenCalled(); }); it('resets form validation message when switching to new payment method', async () => { - const container = mount(); + const container = mount( + , + ); // Submitting a blank form should display some error messages based on the provided validation schema - container.find('form') - .simulate('submit'); + container.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); - expect(container.exists('[data-test="cc-number-field-error-message"]')) - .toEqual(true); + expect(container.exists('[data-test="cc-number-field-error-message"]')).toBe(true); // Selecting a new payment method should clear the error messages act(() => { - const methodList: ReactWrapper = container.find(PaymentMethodList); + const methodList: ReactWrapper = + container.find(PaymentMethodList); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion methodList.prop('onSelect')!(defaultProps.methods[1]); }); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); container.update(); - expect(container.exists('[data-test="cc-number-field-error-message"]')) - .toEqual(false); + expect(container.exists('[data-test="cc-number-field-error-message"]')).toBe(false); }); describe('PaymentSubmitButton', () => { @@ -313,55 +300,66 @@ describe('PaymentForm', () => { it('renders with default label', () => { defaultProps = { ...defaultProps, selectedMethod }; - const container = mount(); + + const container = mount(); const submitButton = container.find(PaymentSubmitButton); - expect(submitButton) - .toHaveLength(1); + expect(submitButton).toHaveLength(1); - expect(submitButton.props()) - .toEqual(expect.objectContaining({ + expect(submitButton.props()).toEqual( + expect.objectContaining({ methodGateway: 'baz', methodId: 'foo', methodType: 'bar', methodName: 'Authorizenet', - })); + }), + ); }); it('renders with default label if selected method is amazonpay and there is paymentToken', () => { - selectedMethod = { ...selectedMethod, id: 'amazonpay', initializationData: { paymentToken: 'foo' } }; + selectedMethod = { + ...selectedMethod, + id: 'amazonpay', + initializationData: { paymentToken: 'foo' }, + }; defaultProps = { ...defaultProps, selectedMethod }; - const container = mount(); + + const container = mount(); const submitButton = container.find(PaymentSubmitButton); - expect(submitButton) - .toHaveLength(1); + expect(submitButton).toHaveLength(1); - expect(submitButton.props()) - .toEqual(expect.objectContaining({ + expect(submitButton.props()).toEqual( + expect.objectContaining({ methodGateway: 'baz', methodId: undefined, methodType: 'bar', methodName: 'Amazon Pay', - })); + }), + ); }); it('renders with special label if selected method is amazonpay and there is no paymentToken', () => { - selectedMethod = { ...selectedMethod, id: 'amazonpay', initializationData: { paymentToken: '' } }; + selectedMethod = { + ...selectedMethod, + id: 'amazonpay', + initializationData: { paymentToken: '' }, + }; defaultProps = { ...defaultProps, selectedMethod }; - const container = mount(); + + const container = mount(); const submitButton = container.find(PaymentSubmitButton); - expect(submitButton) - .toHaveLength(1); + expect(submitButton).toHaveLength(1); - expect(submitButton.props()) - .toEqual(expect.objectContaining({ + expect(submitButton.props()).toEqual( + expect.objectContaining({ methodGateway: 'baz', methodId: 'amazonpay', methodType: 'bar', methodName: 'Amazon Pay', - })); + }), + ); }); }); }); diff --git a/packages/core/src/app/payment/PaymentForm.tsx b/packages/core/src/app/payment/PaymentForm.tsx index ca1eb6c2a7..f9776796f9 100644 --- a/packages/core/src/app/payment/PaymentForm.tsx +++ b/packages/core/src/app/payment/PaymentForm.tsx @@ -1,20 +1,26 @@ -import { PaymentFormValues } from '@bigcommerce/checkout/payment-integration-api'; import { PaymentMethod } from '@bigcommerce/checkout-sdk'; -import { withFormik, FormikProps, WithFormikConfig } from 'formik'; +import { FormikProps, withFormik, WithFormikConfig } from 'formik'; import { isNil, noop, omitBy } from 'lodash'; -import React, { memo, useCallback, useContext, useMemo, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo, useCallback, useContext, useMemo } from 'react'; import { ObjectSchema } from 'yup'; +import { PaymentFormValues } from '@bigcommerce/checkout/payment-integration-api'; + import { withLanguage, WithLanguageProps } from '../locale'; import { TermsConditions } from '../termsConditions'; import { Fieldset, Form, FormContext } from '../ui/form'; import getPaymentValidationSchema from './getPaymentValidationSchema'; -import { getPaymentMethodName, getUniquePaymentMethodId, PaymentMethodId, PaymentMethodList } from './paymentMethod'; -import { StoreCreditField, StoreCreditOverlay } from './storeCredit'; +import { + getPaymentMethodName, + getUniquePaymentMethodId, + PaymentMethodId, + PaymentMethodList, +} from './paymentMethod'; import PaymentRedeemables from './PaymentRedeemables'; import PaymentSubmitButton from './PaymentSubmitButton'; import SpamProtectionField from './SpamProtectionField'; +import { StoreCreditField, StoreCreditOverlay } from './storeCredit'; export interface PaymentFormProps { availableStoreCredit?: number; @@ -43,7 +49,9 @@ export interface PaymentFormProps { onUnhandledError?(error: Error): void; } -const PaymentForm: FunctionComponent & WithLanguageProps> = ({ +const PaymentForm: FunctionComponent< + PaymentFormProps & FormikProps & WithLanguageProps +> = ({ availableStoreCredit = 0, didExceedSpamLimit, isEmbedded, @@ -73,77 +81,89 @@ const PaymentForm: FunctionComponent { - if (!selectedMethod ) { + if (!selectedMethod) { return; } - return selectedMethod?.initializationData?.payPalCreditProductBrandName?.credit || selectedMethod?.initializationData?.payPalCreditProductBrandName; + return ( + selectedMethod.initializationData?.payPalCreditProductBrandName?.credit || + selectedMethod.initializationData?.payPalCreditProductBrandName + ); }, [selectedMethod]); if (shouldExecuteSpamCheck) { - return ; + return ( + + ); } return ( -
    - { usableStoreCredit > 0 && } + + {usableStoreCredit > 0 && ( + + )} - { isTermsConditionsRequired && } + {isTermsConditionsRequired && ( + + )}
    - { shouldHidePaymentSubmitButton ? - : + {shouldHidePaymentSubmitButton ? ( + + ) : ( } + brandName={brandName} + initialisationStrategyType={ + selectedMethod && selectedMethod.initializationStrategy?.type + } + isDisabled={shouldDisableSubmit} + methodGateway={selectedMethod && selectedMethod.gateway} + methodId={selectedMethodId} + methodName={ + selectedMethod && getPaymentMethodName(language)(selectedMethod) + } + methodType={selectedMethod && selectedMethod.method} + /> + )}
    ); @@ -178,14 +198,51 @@ const PaymentMethodListFieldset: FunctionComponent { const { setSubmitted } = useContext(FormContext); - const commonValues = useMemo( - () => ({ terms: values.terms }), - [values.terms] + const commonValues = useMemo(() => ({ terms: values.terms }), [values.terms]); + + const handlePaymentMethodSelect = useCallback( + (method: PaymentMethod) => { + resetForm({ + ...commonValues, + ccCustomerCode: '', + ccCvv: '', + ccDocument: '', + customerEmail: '', + customerMobile: '', + ccExpiry: '', + ccName: '', + ccNumber: '', + instrumentId: '', + paymentProviderRadio: getUniquePaymentMethodId(method.id, method.gateway), + shouldCreateAccount: true, + shouldSaveInstrument: false, + }); + + setSubmitted(false); + onMethodSelect(method); + }, + [commonValues, onMethodSelect, resetForm, setSubmitted], ); - const handlePaymentMethodSelect = useCallback((method: PaymentMethod) => { - resetForm({ - ...commonValues, + return ( +
    + {!isPaymentDataRequired() && } + + +
    + ); +}; + +const paymentFormConfig: WithFormikConfig = + { + mapPropsToValues: ({ defaultGatewayId, defaultMethodId }) => ({ ccCustomerCode: '', ccCvv: '', ccDocument: '', @@ -194,85 +251,43 @@ const PaymentMethodListFieldset: FunctionComponent - { !isPaymentDataRequired() && } - - - - ); -}; - -const paymentFormConfig: WithFormikConfig = { - mapPropsToValues: ({ - defaultGatewayId, - defaultMethodId, - }) => ({ - ccCustomerCode: '', - ccCvv: '', - ccDocument: '', - customerEmail: '', - customerMobile: '', - ccExpiry: '', - ccName: '', - ccNumber: '', - paymentProviderRadio: getUniquePaymentMethodId(defaultMethodId, defaultGatewayId), - instrumentId: '', - shouldCreateAccount: true, - shouldSaveInstrument: false, - terms: false, - hostedForm: { - cardType: '', - errors: { - cardCode: '', - cardCodeVerification: '', - cardExpiry: '', - cardName: '', - cardNumber: '', - cardNumberVerification: '', + terms: false, + hostedForm: { + cardType: '', + errors: { + cardCode: '', + cardCodeVerification: '', + cardExpiry: '', + cardName: '', + cardNumber: '', + cardNumberVerification: '', + }, }, - }, - }), + }), - handleSubmit: (values, { props: { onSubmit = noop } }) => { - onSubmit(omitBy(values, (value, key) => - isNil(value) || value === '' || key === 'hostedForm' - )); - }, + handleSubmit: (values, { props: { onSubmit = noop } }) => { + onSubmit( + omitBy( + values, + (value, key) => isNil(value) || value === '' || key === 'hostedForm', + ), + ); + }, - validationSchema: ({ - language, - isTermsConditionsRequired = false, - validationSchema, - }: PaymentFormProps & WithLanguageProps) => ( - getPaymentValidationSchema({ - additionalValidation: validationSchema, - isTermsConditionsRequired, + validationSchema: ({ language, - }) - ), -}; + isTermsConditionsRequired = false, + validationSchema, + }: PaymentFormProps & WithLanguageProps) => + getPaymentValidationSchema({ + additionalValidation: validationSchema, + isTermsConditionsRequired, + language, + }), + }; export default withLanguage(withFormik(paymentFormConfig)(memo(PaymentForm))); diff --git a/packages/core/src/app/payment/PaymentRedeemables.spec.tsx b/packages/core/src/app/payment/PaymentRedeemables.spec.tsx index 5eff3dcf5c..c03ec59115 100644 --- a/packages/core/src/app/payment/PaymentRedeemables.spec.tsx +++ b/packages/core/src/app/payment/PaymentRedeemables.spec.tsx @@ -1,4 +1,4 @@ -import { createCheckoutService, CheckoutService } from '@bigcommerce/checkout-sdk'; +import { CheckoutService, createCheckoutService } from '@bigcommerce/checkout-sdk'; import { mount } from 'enzyme'; import React from 'react'; @@ -15,32 +15,28 @@ describe('PaymentRedeemables', () => { beforeEach(() => { checkoutService = createCheckoutService(); - jest.spyOn(checkoutService.getState().data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutService.getState().data, 'getConfig').mockReturnValue(getStoreConfig()); - jest.spyOn(checkoutService.getState().data, 'getCheckout') - .mockReturnValue(getCheckout()); + jest.spyOn(checkoutService.getState().data, 'getCheckout').mockReturnValue(getCheckout()); }); it('renders redeemable component with container fieldset', () => { const container = mount( - + - + , ); - expect(container.exists('fieldset.redeemable-payments')) - .toEqual(true); + expect(container.exists('fieldset.redeemable-payments')).toBe(true); }); it('renders redeemable component with applied redeemables', () => { const container = mount( - + - + , ); - expect(container.find(Redeemable).prop('showAppliedRedeemables')) - .toEqual(true); + expect(container.find(Redeemable).prop('showAppliedRedeemables')).toBe(true); }); }); diff --git a/packages/core/src/app/payment/PaymentRedeemables.tsx b/packages/core/src/app/payment/PaymentRedeemables.tsx index 4e85538cff..5a1ac74300 100644 --- a/packages/core/src/app/payment/PaymentRedeemables.tsx +++ b/packages/core/src/app/payment/PaymentRedeemables.tsx @@ -1,15 +1,12 @@ -import React, { memo, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo } from 'react'; import { mapToRedeemableProps, Redeemable, RedeemableProps } from '../cart'; import { withCheckout } from '../checkout'; import { Fieldset } from '../ui/form'; -const PaymentRedeemables: FunctionComponent = redeemableProps => ( +const PaymentRedeemables: FunctionComponent = (redeemableProps) => (
    - +
    ); diff --git a/packages/core/src/app/payment/PaymentSubmitButton.spec.tsx b/packages/core/src/app/payment/PaymentSubmitButton.spec.tsx index 6b2714ede7..8f34b4a102 100644 --- a/packages/core/src/app/payment/PaymentSubmitButton.spec.tsx +++ b/packages/core/src/app/payment/PaymentSubmitButton.spec.tsx @@ -1,4 +1,4 @@ -import { createCheckoutService, CheckoutService, LanguageService } from '@bigcommerce/checkout-sdk'; +import { CheckoutService, createCheckoutService, LanguageService } from '@bigcommerce/checkout-sdk'; import { mount, render } from 'enzyme'; import React, { FunctionComponent } from 'react'; @@ -22,169 +22,154 @@ describe('PaymentSubmitButton', () => { localeContext = createLocaleContext(getStoreConfig()); languageService = localeContext.language; - PaymentSubmitButtonTest = props => ( - - - + PaymentSubmitButtonTest = (props) => ( + + + ); }); it('matches snapshot with rendered output', () => { - const component = render( - - ); + const component = render(); - expect(component) - .toMatchSnapshot(); + expect(component).toMatchSnapshot(); }); it('forwards props to button', () => { - const component = mount( - - ); + const component = mount(); - expect(component.find(Button).props()) - .toEqual(expect.objectContaining({ + expect(component.find(Button).props()).toEqual( + expect.objectContaining({ disabled: true, - })); + }), + ); }); it('renders button with default label', () => { - const component = mount( - - ); + const component = mount(); - expect(component.text()) - .toEqual(languageService.translate('payment.place_order_action')); + expect(component.text()).toEqual(languageService.translate('payment.place_order_action')); }); it('renders button with special label for Amazon', () => { - const component = mount( - - ); + const component = mount(); - expect(component.text()) - .toEqual(languageService.translate('payment.amazon_continue_action')); + expect(component.text()).toEqual( + languageService.translate('payment.amazon_continue_action'), + ); }); it('renders button with special label for Amazon Pay', () => { - const component = mount( - - ); + const component = mount(); - expect(component.text()) - .toEqual(languageService.translate('payment.amazonpay_continue_action')); + expect(component.text()).toEqual( + languageService.translate('payment.amazonpay_continue_action'), + ); }); it('renders button with special label and icon for Bolt', () => { - const component = mount( - - ); + const component = mount(); - expect(component.text()) - .toEqual('Bolt' + languageService.translate('payment.place_order_action')); - expect(component.find(IconBolt).length) - .toEqual(1); + expect(component.text()).toBe( + `Bolt${languageService.translate('payment.place_order_action')}`, + ); + expect(component.find(IconBolt)).toHaveLength(1); }); it('renders button with special label for Barclaycard', () => { - const component = mount( - - ); + const component = mount(); - expect(component.text()) - .toEqual(languageService.translate('payment.barclaycard_continue_action')); + expect(component.text()).toEqual( + languageService.translate('payment.barclaycard_continue_action'), + ); }); it('renders button with special label for Visa Checkout provided by Braintree', () => { - const component = mount( - - ); + const component = mount(); - expect(component.text()) - .toEqual(languageService.translate('payment.visa_checkout_continue_action')); + expect(component.text()).toEqual( + languageService.translate('payment.visa_checkout_continue_action'), + ); }); it('renders button with special label for ChasePay', () => { - const component = mount( - - ); + const component = mount(); - expect(component.text()) - .toEqual(languageService.translate('payment.chasepay_continue_action')); + expect(component.text()).toEqual( + languageService.translate('payment.chasepay_continue_action'), + ); }); it('renders button with special label for Opy', () => { - const component = mount( - - ); + const component = mount(); - expect(component.text()) - .toEqual(languageService.translate('payment.opy_continue_action', { methodName: 'Opy' })); + expect(component.text()).toEqual( + languageService.translate('payment.opy_continue_action', { methodName: 'Opy' }), + ); }); it('renders button with special label for PayPal', () => { - const component = mount( - - ); + const component = mount(); - expect(component.text()) - .toEqual(languageService.translate('payment.paypal_continue_action')); + expect(component.text()).toEqual( + languageService.translate('payment.paypal_continue_action'), + ); }); it('renders button with special label for Braintree Venmo', () => { const component = mount( - + , ); - expect(component.text()) - .toEqual(languageService.translate('payment.paypal_venmo_continue_action')); + expect(component.text()).toEqual( + languageService.translate('payment.paypal_venmo_continue_action'), + ); }); it('renders button with special label for PayPal Venmo', () => { const component = mount( - + , ); - expect(component.text()) - .toEqual(languageService.translate('payment.paypal_venmo_continue_action')); + expect(component.text()).toEqual( + languageService.translate('payment.paypal_venmo_continue_action'), + ); }); it('renders button with special label for PayPal Credit', () => { - const component = mount( - - ); + const component = mount(); - expect(component.text()) - .toEqual(languageService.translate('payment.paypal_pay_later_continue_action')); + expect(component.text()).toEqual( + languageService.translate('payment.paypal_pay_later_continue_action'), + ); }); it('renders button with special label for Quadpay', () => { - const component = mount( - - ); + const component = mount(); - expect(component.text()) - .toEqual(languageService.translate('payment.quadpay_continue_action')); + expect(component.text()).toEqual( + languageService.translate('payment.quadpay_continue_action'), + ); }); it('renders button with special label for Zip', () => { - const component = mount( - - ); + const component = mount(); - expect(component.text()) - .toEqual(languageService.translate('payment.zip_continue_action')); + expect(component.text()).toEqual(languageService.translate('payment.zip_continue_action')); }); it('renders button with label of "Continue with ${methodName}"', () => { const component = mount( - + , ); - expect(component.text()) - .toEqual(languageService.translate('payment.ppsdk_continue_action', { methodName: 'Foo' })); + expect(component.text()).toEqual( + languageService.translate('payment.ppsdk_continue_action', { methodName: 'Foo' }), + ); }); }); diff --git a/packages/core/src/app/payment/PaymentSubmitButton.tsx b/packages/core/src/app/payment/PaymentSubmitButton.tsx index 68443650e7..f1e4824b9f 100644 --- a/packages/core/src/app/payment/PaymentSubmitButton.tsx +++ b/packages/core/src/app/payment/PaymentSubmitButton.tsx @@ -1,4 +1,4 @@ -import React, { memo, Fragment, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo } from 'react'; import { withCheckout } from '../checkout'; import { TranslatedString } from '../locale'; @@ -18,73 +18,95 @@ interface PaymentSubmitButtonTextProps { const providersWithCustomClasses = [PaymentMethodId.Bolt]; -const PaymentSubmitButtonText: FunctionComponent = memo(({ methodId, methodName, methodType, methodGateway, initialisationStrategyType, brandName }) => { - - if (methodName && initialisationStrategyType === 'none') { - return ; - } - - if (methodId === PaymentMethodId.Amazon) { - return ; - } - - if (methodId === PaymentMethodId.AmazonPay) { - return ; - } - - if (methodId === PaymentMethodId.Bolt) { - return ( - - - ); - } - - if (methodGateway === PaymentMethodId.Barclaycard) { - return ; - } - - if (methodGateway === PaymentMethodId.BlueSnapV2) { - return ; - } - - if (methodType === PaymentMethodType.VisaCheckout) { - return ; - } - - if (methodType === PaymentMethodType.Chasepay) { - return ; - } - - if (methodType === PaymentMethodType.PaypalVenmo || methodId === PaymentMethodId.BraintreeVenmo) { - return ; - } - - if (methodType === PaymentMethodType.Paypal) { - return ; - } - - if (methodType === PaymentMethodType.PaypalCredit) { - return ; - } - - if (methodId === PaymentMethodId.Opy) { - return ; - } - - if (methodId === PaymentMethodId.Quadpay) { - return ; - } - - if (methodId === PaymentMethodId.Zip) { - return ; - } - - if (methodId === PaymentMethodId.Klarna) { - return ; - } - - return ; -}); +const PaymentSubmitButtonText: FunctionComponent = memo( + ({ + methodId, + methodName, + methodType, + methodGateway, + initialisationStrategyType, + brandName, + }) => { + if (methodName && initialisationStrategyType === 'none') { + return ; + } + + if (methodId === PaymentMethodId.Amazon) { + return ; + } + + if (methodId === PaymentMethodId.AmazonPay) { + return ; + } + + if (methodId === PaymentMethodId.Bolt) { + return ( + <> + + + + ); + } + + if (methodGateway === PaymentMethodId.Barclaycard) { + return ; + } + + if (methodGateway === PaymentMethodId.BlueSnapV2) { + return ; + } + + if (methodType === PaymentMethodType.VisaCheckout) { + return ; + } + + if (methodType === PaymentMethodType.Chasepay) { + return ; + } + + if ( + methodType === PaymentMethodType.PaypalVenmo || + methodId === PaymentMethodId.BraintreeVenmo + ) { + return ; + } + + if (methodType === PaymentMethodType.Paypal) { + return ; + } + + if (methodType === PaymentMethodType.PaypalCredit) { + return ( + + ); + } + + if (methodId === PaymentMethodId.Opy) { + return ; + } + + if (methodId === PaymentMethodId.Quadpay) { + return ; + } + + if (methodId === PaymentMethodId.Zip) { + return ; + } + + if (methodId === PaymentMethodId.Klarna) { + return ; + } + + return ; + }, +); export interface PaymentSubmitButtonProps { methodGateway?: string; @@ -101,7 +123,9 @@ interface WithCheckoutPaymentSubmitButtonProps { isSubmitting?: boolean; } -const PaymentSubmitButton: FunctionComponent = ({ +const PaymentSubmitButton: FunctionComponent< + PaymentSubmitButtonProps & WithCheckoutPaymentSubmitButtonProps +> = ({ isDisabled, isInitializing, isSubmitting, @@ -112,34 +136,34 @@ const PaymentSubmitButton: FunctionComponent ( - - ); + +); export default withCheckout(({ checkoutState }) => { const { - statuses: { - isInitializingCustomer, - isInitializingPayment, - isSubmittingOrder, - }, + statuses: { isInitializingCustomer, isInitializingPayment, isSubmittingOrder }, } = checkoutState; return { diff --git a/packages/core/src/app/payment/SpamProtectionField.spec.tsx b/packages/core/src/app/payment/SpamProtectionField.spec.tsx index c301e79819..dc5f1607fe 100644 --- a/packages/core/src/app/payment/SpamProtectionField.spec.tsx +++ b/packages/core/src/app/payment/SpamProtectionField.spec.tsx @@ -1,4 +1,4 @@ -import { createCheckoutService, CheckoutService, StandardError } from '@bigcommerce/checkout-sdk'; +import { CheckoutService, createCheckoutService, StandardError } from '@bigcommerce/checkout-sdk'; import { mount, render } from 'enzyme'; import React, { FunctionComponent } from 'react'; @@ -13,79 +13,73 @@ describe('SpamProtectionField', () => { beforeEach(() => { checkoutService = createCheckoutService(); - jest.spyOn(checkoutService, 'executeSpamCheck') - .mockResolvedValue(checkoutService.getState()); + jest.spyOn(checkoutService, 'executeSpamCheck').mockResolvedValue( + checkoutService.getState(), + ); - SpamProtectionTest = props => ( - - + SpamProtectionTest = (props) => ( + + ); }); it('renders spam protection field', () => { - expect(render()) - .toMatchSnapshot(); + expect(render()).toMatchSnapshot(); }); it('notifies parent component if unable to verify', async () => { const handleError = jest.fn(); const error = { type: 'test error' }; - jest.spyOn(checkoutService, 'executeSpamCheck') - .mockRejectedValue(error); + jest.spyOn(checkoutService, 'executeSpamCheck').mockRejectedValue(error); - const component = mount(); + const component = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(handleError) - .toHaveBeenCalledWith(error); + expect(handleError).toHaveBeenCalledWith(error); }); it('does not notify parent component if unable to verify because of cancellation by user', async () => { const handleError = jest.fn(); const error = { type: 'spam_protection_challenge_not_completed' }; - jest.spyOn(checkoutService, 'executeSpamCheck') - .mockRejectedValue(error); + jest.spyOn(checkoutService, 'executeSpamCheck').mockRejectedValue(error); - const component = mount(); + const component = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(handleError) - .not.toHaveBeenCalledWith(error); + expect(handleError).not.toHaveBeenCalledWith(error); }); describe('if have not exceeded limit', () => { it('executes spam check on mount', () => { mount(); - expect(checkoutService.executeSpamCheck) - .toHaveBeenCalled(); + expect(checkoutService.executeSpamCheck).toHaveBeenCalled(); }); it('does not render verify message', () => { const component = mount(); - expect(component.exists('[data-test="spam-protection-verify-button"]')) - .toBeFalsy(); + expect(component.exists('[data-test="spam-protection-verify-button"]')).toBeFalsy(); }); it('renders verify message if there is error', async () => { - jest.spyOn(checkoutService, 'executeSpamCheck') - .mockRejectedValue(new Error('Unknown error')); + jest.spyOn(checkoutService, 'executeSpamCheck').mockRejectedValue( + new Error('Unknown error'), + ); const component = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); component.update(); - expect(component.exists('[data-test="spam-protection-verify-button"]')) - .toBeTruthy(); + expect(component.exists('[data-test="spam-protection-verify-button"]')).toBeTruthy(); }); }); @@ -93,25 +87,21 @@ describe('SpamProtectionField', () => { it('does not execute spam check on mount', () => { mount(); - expect(checkoutService.executeSpamCheck) - .not.toHaveBeenCalled(); + expect(checkoutService.executeSpamCheck).not.toHaveBeenCalled(); }); it('executes spam check on click', () => { const component = mount(); - component.find('[data-test="spam-protection-verify-button"]') - .simulate('click'); + component.find('[data-test="spam-protection-verify-button"]').simulate('click'); - expect(checkoutService.executeSpamCheck) - .toHaveBeenCalled(); + expect(checkoutService.executeSpamCheck).toHaveBeenCalled(); }); it('renders verify message', () => { const component = mount(); - expect(component.exists('[data-test="spam-protection-verify-button"]')) - .toBeTruthy(); + expect(component.exists('[data-test="spam-protection-verify-button"]')).toBeTruthy(); }); }); }); diff --git a/packages/core/src/app/payment/SpamProtectionField.tsx b/packages/core/src/app/payment/SpamProtectionField.tsx index 77d7d6e154..4ed1d71e9b 100644 --- a/packages/core/src/app/payment/SpamProtectionField.tsx +++ b/packages/core/src/app/payment/SpamProtectionField.tsx @@ -2,7 +2,7 @@ import { CheckoutSelectors } from '@bigcommerce/checkout-sdk'; import { noop } from 'lodash'; import React, { Component, MouseEvent, ReactNode } from 'react'; -import { withCheckout, CheckoutContextProps } from '../checkout'; +import { CheckoutContextProps, withCheckout } from '../checkout'; import { isErrorWithType } from '../common/error'; import { TranslatedString } from '../locale'; import { LoadingOverlay } from '../ui/loading'; @@ -21,9 +21,10 @@ interface WithCheckoutSpamProtectionProps { executeSpamCheck(): Promise; } -function mapToSpamProtectionProps( - { checkoutService, checkoutState }: CheckoutContextProps -): WithCheckoutSpamProtectionProps { +function mapToSpamProtectionProps({ + checkoutService, + checkoutState, +}: CheckoutContextProps): WithCheckoutSpamProtectionProps { return { isExecutingSpamCheck: checkoutState.statuses.isExecutingSpamCheck(), executeSpamCheck: checkoutService.executeSpamCheck, @@ -53,8 +54,8 @@ class SpamProtectionField extends Component< return (
    - - { this.renderContent() } + + {this.renderContent()}
    ); @@ -68,24 +69,21 @@ class SpamProtectionField extends Component< return; } - return
    - - - -
    ; + return ( +
    + + + +
    + ); } private async verify(): Promise { - const { - executeSpamCheck, - onUnhandledError = noop, - } = this.props; + const { executeSpamCheck, onUnhandledError = noop } = this.props; try { await executeSpamCheck(); @@ -93,13 +91,16 @@ class SpamProtectionField extends Component< this.setState({ shouldShowRetryButton: true }); // Notify the parent component if the user experiences a problem other than cancelling the reCaptcha challenge. - if (isErrorWithType(error) && error.type !== 'spam_protection_challenge_not_completed') { + if ( + isErrorWithType(error) && + error.type !== 'spam_protection_challenge_not_completed' + ) { onUnhandledError(error); } } } - private handleRetry: (event: MouseEvent) => void = event => { + private handleRetry: (event: MouseEvent) => void = (event) => { event.preventDefault(); this.verify(); diff --git a/packages/core/src/app/payment/StoreInstrumentFieldset/InstrumentStorageField.tsx b/packages/core/src/app/payment/StoreInstrumentFieldset/InstrumentStorageField.tsx index 2a12a1930b..c8b6b3f51c 100644 --- a/packages/core/src/app/payment/StoreInstrumentFieldset/InstrumentStorageField.tsx +++ b/packages/core/src/app/payment/StoreInstrumentFieldset/InstrumentStorageField.tsx @@ -1,4 +1,4 @@ -import React, { memo, useMemo, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo, useMemo } from 'react'; import { TranslatedString } from '../../locale'; import { CheckboxFormField } from '../../ui/form'; @@ -7,20 +7,22 @@ interface InstrumentStorageFieldProps { isAccountInstrument: boolean; } -const InstrumentStorageField: FunctionComponent = ({ isAccountInstrument }) => { - const translationId = isAccountInstrument ? - 'payment.account_instrument_save_payment_method_label' : - 'payment.instrument_save_payment_method_label'; +const InstrumentStorageField: FunctionComponent = ({ + isAccountInstrument, +}) => { + const translationId = isAccountInstrument + ? 'payment.account_instrument_save_payment_method_label' + : 'payment.instrument_save_payment_method_label'; - const labelContent = useMemo(() => ( - - ), [translationId]); + const labelContent = useMemo(() => , [translationId]); - return ; + return ( + + ); }; export default memo(InstrumentStorageField); diff --git a/packages/core/src/app/payment/StoreInstrumentFieldset/InstrumentStoreAsDefaultField.tsx b/packages/core/src/app/payment/StoreInstrumentFieldset/InstrumentStoreAsDefaultField.tsx index 60fe1a5a93..8cdd352e5d 100644 --- a/packages/core/src/app/payment/StoreInstrumentFieldset/InstrumentStoreAsDefaultField.tsx +++ b/packages/core/src/app/payment/StoreInstrumentFieldset/InstrumentStoreAsDefaultField.tsx @@ -1,4 +1,4 @@ -import React, { memo, useMemo, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo, useMemo } from 'react'; import { TranslatedString } from '../../locale'; import { CheckboxFormField } from '../../ui/form'; @@ -8,21 +8,24 @@ interface InstrumentStoreAsDefaultFieldProps { disabled?: boolean; } -const InstrumentStoreAsDefaultField: FunctionComponent = ({ isAccountInstrument, disabled = false }) => { - const translationId = isAccountInstrument ? - 'payment.account_instrument_save_as_default_payment_method_label' : - 'payment.instrument_save_as_default_payment_method_label'; +const InstrumentStoreAsDefaultField: FunctionComponent = ({ + isAccountInstrument, + disabled = false, +}) => { + const translationId = isAccountInstrument + ? 'payment.account_instrument_save_as_default_payment_method_label' + : 'payment.instrument_save_as_default_payment_method_label'; - const labelContent = useMemo(() => ( - - ), [translationId]); + const labelContent = useMemo(() => , [translationId]); - return ; + return ( + + ); }; export default memo(InstrumentStoreAsDefaultField); diff --git a/packages/core/src/app/payment/StoreInstrumentFieldset/StoreInstrumentFieldset.spec.tsx b/packages/core/src/app/payment/StoreInstrumentFieldset/StoreInstrumentFieldset.spec.tsx index a512f3826d..81bfe83244 100644 --- a/packages/core/src/app/payment/StoreInstrumentFieldset/StoreInstrumentFieldset.spec.tsx +++ b/packages/core/src/app/payment/StoreInstrumentFieldset/StoreInstrumentFieldset.spec.tsx @@ -1,4 +1,8 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, +} from '@bigcommerce/checkout-sdk'; import { mount } from 'enzyme'; import { Formik } from 'formik'; import { merge, noop } from 'lodash'; @@ -21,21 +25,19 @@ describe('StoreInstrumentFieldset', () => { checkoutService = createCheckoutService(); checkoutState = checkoutService.getState(); - jest.spyOn(checkoutState.data, 'getPaymentMethod') - .mockReturnValue(merge({}, getPaymentMethod(), { + jest.spyOn(checkoutState.data, 'getPaymentMethod').mockReturnValue( + merge({}, getPaymentMethod(), { config: { isVaultingEnabled: true, }, - })); - - StoreInstrumentFieldsetTest = props => ( - - - - + }), + ); + + StoreInstrumentFieldsetTest = (props) => ( + + + + @@ -44,8 +46,7 @@ describe('StoreInstrumentFieldset', () => { describe('when there are no previously stored instruments', () => { beforeEach(() => { - jest.spyOn(checkoutState.data, 'getInstruments') - .mockReturnValue([]); + jest.spyOn(checkoutState.data, 'getInstruments').mockReturnValue([]); }); describe('when using a new card', () => { @@ -56,44 +57,42 @@ describe('StoreInstrumentFieldset', () => { expect(container.text()).toMatch(/card/i); expect(container.text()).not.toMatch(/account/i); - expect(container.find('input[name="shouldSaveInstrument"]').exists()) - .toBe(true); + expect(container.find('input[name="shouldSaveInstrument"]').exists()).toBe(true); }); it('does not show the "make default" input', () => { const container = mount(); - expect(container.find('input[name="shouldSetAsDefaultInstrument"]').exists()) - .toBe(false); + expect(container.find('input[name="shouldSetAsDefaultInstrument"]').exists()).toBe( + false, + ); }); }); describe('when using a new account instrument', () => { it('shows the "save account instrument" input', () => { - const container = mount(); + const container = mount(); expect(container.text()).toMatch(/save/i); expect(container.text()).toMatch(/account/i); expect(container.text()).not.toMatch(/card/i); - expect(container.find('input[name="shouldSaveInstrument"]').exists()) - .toBe(true); + expect(container.find('input[name="shouldSaveInstrument"]').exists()).toBe(true); }); it('does not show the "make default" input', () => { - const container = mount(); + const container = mount(); - expect(container.find('input[name="shouldSetAsDefaultInstrument"]').exists()) - .toBe(false); + expect(container.find('input[name="shouldSetAsDefaultInstrument"]').exists()).toBe( + false, + ); }); }); - }); describe('when there are some previously stored instruments', () => { beforeEach(() => { - jest.spyOn(checkoutState.data, 'getInstruments') - .mockReturnValue(getInstruments()); + jest.spyOn(checkoutState.data, 'getInstruments').mockReturnValue(getInstruments()); }); describe('when using a new card', () => { @@ -105,114 +104,123 @@ describe('StoreInstrumentFieldset', () => { expect(container.text()).toMatch(/card/i); expect(container.text()).not.toMatch(/account/i); - expect(container.find('input[name="shouldSaveInstrument"]').exists()) - .toBe(true); - expect(container.find('input[name="shouldSetAsDefaultInstrument"]').exists()) - .toBe(true); + expect(container.find('input[name="shouldSaveInstrument"]').exists()).toBe(true); + expect(container.find('input[name="shouldSetAsDefaultInstrument"]').exists()).toBe( + true, + ); }); }); describe('when using a new account instrument', () => { it('shows the both the "save account instrument" and "make default" inputs', () => { - const container = mount(); + const container = mount(); expect(container.text()).toMatch(/save/i); expect(container.text()).toMatch(/default/i); expect(container.text()).toMatch(/account/i); expect(container.text()).not.toMatch(/card/i); - expect(container.find('input[name="shouldSaveInstrument"]').exists()) - .toBe(true); + expect(container.find('input[name="shouldSaveInstrument"]').exists()).toBe(true); }); }); describe('when using a previously stored, default card', () => { it('does not show either the "save card" or "make default" inputs', () => { - const container = mount(); - - expect(container.find('input[name="shouldSaveInstrument"]').exists()) - .toBe(false); - expect(container.find('input[name="shouldSetAsDefaultInstrument"]').exists()) - .toBe(false); + const container = mount( + , + ); + + expect(container.find('input[name="shouldSaveInstrument"]').exists()).toBe(false); + expect(container.find('input[name="shouldSetAsDefaultInstrument"]').exists()).toBe( + false, + ); }); }); describe('when using a previously stored, default account instrument', () => { it('does not show either the "save account instrument" or "make default" inputs', () => { - jest.spyOn(checkoutState.data, 'getInstruments') - .mockReturnValue([ - { - bigpayToken: '4123', - provider: 'authorizenet', - externalId: 'test@external-id-2.com', - trustedShippingAddress: false, - defaultInstrument: true, - method: 'paypal', - type: 'account', - }, - ]); - - const container = mount(); - - expect(container.find('input[name="shouldSaveInstrument"]').exists()) - .toBe(false); - expect(container.find('input[name="shouldSetAsDefaultInstrument"]').exists()) - .toBe(false); + jest.spyOn(checkoutState.data, 'getInstruments').mockReturnValue([ + { + bigpayToken: '4123', + provider: 'authorizenet', + externalId: 'test@external-id-2.com', + trustedShippingAddress: false, + defaultInstrument: true, + method: 'paypal', + type: 'account', + }, + ]); + + const container = mount( + , + ); + + expect(container.find('input[name="shouldSaveInstrument"]').exists()).toBe(false); + expect(container.find('input[name="shouldSetAsDefaultInstrument"]').exists()).toBe( + false, + ); }); }); describe('when using a previously stored, but not default card', () => { it('does not show the "save card" input', () => { - const container = mount(); + const container = mount( + , + ); - expect(container.find('input[name="shouldSaveInstrument"]').exists()) - .toBe(false); + expect(container.find('input[name="shouldSaveInstrument"]').exists()).toBe(false); }); it('shows the "make default" input', () => { - const container = mount(); + const container = mount( + , + ); expect(container.text()).toMatch(/default/i); expect(container.text()).toMatch(/card/i); expect(container.text()).not.toMatch(/account/i); - expect(container.find('input[name="shouldSetAsDefaultInstrument"]').exists()) - .toBe(true); + expect(container.find('input[name="shouldSetAsDefaultInstrument"]').exists()).toBe( + true, + ); }); }); describe('when using a previously stored, but not default account instrument', () => { beforeEach(() => { - jest.spyOn(checkoutState.data, 'getInstruments') - .mockReturnValue([ - { - bigpayToken: '4123', - provider: 'authorizenet', - externalId: 'test@external-id-2.com', - trustedShippingAddress: false, - defaultInstrument: false, - method: 'paypal', - type: 'account', - }, - ]); + jest.spyOn(checkoutState.data, 'getInstruments').mockReturnValue([ + { + bigpayToken: '4123', + provider: 'authorizenet', + externalId: 'test@external-id-2.com', + trustedShippingAddress: false, + defaultInstrument: false, + method: 'paypal', + type: 'account', + }, + ]); }); it('does not show the "save account instrument" input', () => { - const container = mount(); + const container = mount( + , + ); - expect(container.find('input[name="shouldSaveInstrument"]').exists()) - .toBe(false); + expect(container.find('input[name="shouldSaveInstrument"]').exists()).toBe(false); }); it('shows the "make default" input', () => { - const container = mount(); + const container = mount( + , + ); expect(container.text()).toMatch(/default/i); expect(container.text()).toMatch(/account/i); expect(container.text()).not.toMatch(/card/i); - expect(container.find('input[name="shouldSetAsDefaultInstrument"]').exists()) - .toBe(true); + expect(container.find('input[name="shouldSetAsDefaultInstrument"]').exists()).toBe( + true, + ); }); }); }); diff --git a/packages/core/src/app/payment/StoreInstrumentFieldset/StoreInstrumentFieldset.tsx b/packages/core/src/app/payment/StoreInstrumentFieldset/StoreInstrumentFieldset.tsx index 08e932b595..03554ebd50 100644 --- a/packages/core/src/app/payment/StoreInstrumentFieldset/StoreInstrumentFieldset.tsx +++ b/packages/core/src/app/payment/StoreInstrumentFieldset/StoreInstrumentFieldset.tsx @@ -1,6 +1,6 @@ import React, { FunctionComponent } from 'react'; -import { withCheckout, CheckoutContextProps } from '../../checkout'; +import { CheckoutContextProps, withCheckout } from '../../checkout'; import { connectFormik, ConnectFormikProps } from '../../common/form'; import { Fieldset } from '../../ui/form'; @@ -22,29 +22,22 @@ type WithFormValues = ConnectFormikProps<{ shouldSaveInstrument: boolean }>; const StoreInstrumentFieldset: FunctionComponent< StoreInstrumentFieldsetProps & WithStorageSettings -> = ({ - showSave, - showSetAsDefault, - isAccountInstrument = false, - setAsDefaultEnabled, -}) => ( +> = ({ showSave, showSetAsDefault, isAccountInstrument = false, setAsDefaultEnabled }) => (
    - { showSave && ( - - ) } + {showSave && } - { showSetAsDefault && ( + {showSetAsDefault && ( - ) } + )}
    ); const mapToProps = ( context: CheckoutContextProps, - props: StoreInstrumentFieldsetProps & WithFormValues + props: StoreInstrumentFieldsetProps & WithFormValues, ): WithStorageSettings | null => { const { checkoutState: { @@ -64,14 +57,14 @@ const mapToProps = ( const addingNewInstrument = !instrumentId; const hasAnyOtherInstruments = !!allInstruments && allInstruments.length > 0; const instrument = - allInstruments && - allInstruments.find(({ bigpayToken }) => bigpayToken === instrumentId); + allInstruments && allInstruments.find(({ bigpayToken }) => bigpayToken === instrumentId); return { ...props, showSave: addingNewInstrument, showSetAsDefault: - (addingNewInstrument && hasAnyOtherInstruments) || Boolean(instrument && !instrument.defaultInstrument), + (addingNewInstrument && hasAnyOtherInstruments) || + Boolean(instrument && !instrument.defaultInstrument), setAsDefaultEnabled: !addingNewInstrument || saveIsChecked, }; }; diff --git a/packages/core/src/app/payment/checkoutcomFieldsets/getCheckoutcomFieldsetValidationSchemas.spec.ts b/packages/core/src/app/payment/checkoutcomFieldsets/getCheckoutcomFieldsetValidationSchemas.spec.ts index 0ea6ef0341..44dadcd4f6 100644 --- a/packages/core/src/app/payment/checkoutcomFieldsets/getCheckoutcomFieldsetValidationSchemas.spec.ts +++ b/packages/core/src/app/payment/checkoutcomFieldsets/getCheckoutcomFieldsetValidationSchemas.spec.ts @@ -26,6 +26,7 @@ describe('getCheckoutcomFieldsetValidationSchemas', () => { describe('sepa validation schema', () => { let sepaValidationSchema: ObjectSchema; + beforeEach(() => { sepaValidationSchema = getCheckoutcomFieldsetValidationSchemas({ paymentMethod: 'sepa', @@ -35,34 +36,42 @@ describe('getCheckoutcomFieldsetValidationSchemas', () => { it('resolves if valid value', async () => { const spy = jest.fn(); - await sepaValidationSchema.validate({ - ...getFormfields.sepa(), - }).then(spy); + + await sepaValidationSchema + .validate({ + ...getFormfields.sepa(), + }) + .then(spy); expect(spy).toHaveBeenCalled(); }); it('throws error if iban is not present', async () => { - const errors = await sepaValidationSchema.validate({ - ...getFormfields.sepa(), - iban: '', - }).catch((error: ValidationError) => error.message); - - expect(errors).toEqual('iban is a required field'); + const errors = await sepaValidationSchema + .validate({ + ...getFormfields.sepa(), + iban: '', + }) + .catch((error: ValidationError) => error.message); + + expect(errors).toBe('iban is a required field'); }); it('throws error if sepaMandate is false', async () => { - const errors = await sepaValidationSchema.validate({ - ...getFormfields.sepa(), - sepaMandate: undefined, - }).catch((error: ValidationError) => error.message); - - expect(errors).toEqual('sepaMandate is a required field'); + const errors = await sepaValidationSchema + .validate({ + ...getFormfields.sepa(), + sepaMandate: undefined, + }) + .catch((error: ValidationError) => error.message); + + expect(errors).toBe('sepaMandate is a required field'); }); }); describe('ccDocument validation schema', () => { let ccDocumentValidationSchema: ObjectSchema; + beforeEach(() => { ccDocumentValidationSchema = getCheckoutcomFieldsetValidationSchemas({ paymentMethod: 'oxxo', @@ -72,24 +81,30 @@ describe('getCheckoutcomFieldsetValidationSchemas', () => { it('resolves if valid value', async () => { const spy = jest.fn(); - await ccDocumentValidationSchema.validate({ - ...getFormfields.oxxo(), - }).then(spy); + + await ccDocumentValidationSchema + .validate({ + ...getFormfields.oxxo(), + }) + .then(spy); expect(spy).toHaveBeenCalled(); }); it('throws error if ccDocument is incomplete', async () => { - const errors = await ccDocumentValidationSchema.validate({ - ccDocument: 'DOJH010199HJLZQQ', - }).catch((error: ValidationError) => error.message); + const errors = await ccDocumentValidationSchema + .validate({ + ccDocument: 'DOJH010199HJLZQQ', + }) + .catch((error: ValidationError) => error.message); - expect(errors).toEqual('ccDocument must be exactly 18 characters'); + expect(errors).toBe('ccDocument must be exactly 18 characters'); }); }); describe('iDeal validation schema', () => { let iDealValidationSchema: ObjectSchema; + beforeEach(() => { iDealValidationSchema = getCheckoutcomFieldsetValidationSchemas({ paymentMethod: 'ideal', @@ -99,19 +114,24 @@ describe('getCheckoutcomFieldsetValidationSchemas', () => { it('resolves if valid value', async () => { const spy = jest.fn(); - await iDealValidationSchema.validate({ - ...getFormfields.ideal(), - }).then(spy); + + await iDealValidationSchema + .validate({ + ...getFormfields.ideal(), + }) + .then(spy); expect(spy).toHaveBeenCalled(); }); it('throws error if iban is not present', async () => { - const errors = await iDealValidationSchema.validate({ - bic: '', - }).catch((error: ValidationError) => error.message); + const errors = await iDealValidationSchema + .validate({ + bic: '', + }) + .catch((error: ValidationError) => error.message); - expect(errors).toEqual('bic is a required field'); + expect(errors).toBe('bic is a required field'); }); }); }); diff --git a/packages/core/src/app/payment/checkoutcomFieldsets/getCheckoutcomFieldsetValidationSchemas.tsx b/packages/core/src/app/payment/checkoutcomFieldsets/getCheckoutcomFieldsetValidationSchemas.tsx index fdacabe08e..9a35d0040e 100644 --- a/packages/core/src/app/payment/checkoutcomFieldsets/getCheckoutcomFieldsetValidationSchemas.tsx +++ b/packages/core/src/app/payment/checkoutcomFieldsets/getCheckoutcomFieldsetValidationSchemas.tsx @@ -1,8 +1,13 @@ import { LanguageService } from '@bigcommerce/checkout-sdk'; import { memoize } from '@bigcommerce/memoize'; -import { boolean, object, string, ObjectSchema } from 'yup'; +import { boolean, object, ObjectSchema, string } from 'yup'; -import { DocumentOnlyCustomFormFieldsetValues, FawryCustomFormFieldsetValues, IdealCustomFormFieldsetValues, SepaCustomFormFieldsetValues } from './CheckoutcomFormValues'; +import { + DocumentOnlyCustomFormFieldsetValues, + FawryCustomFormFieldsetValues, + IdealCustomFormFieldsetValues, + SepaCustomFormFieldsetValues, +} from './CheckoutcomFormValues'; export type checkoutcomCustomPaymentMethods = 'fawry' | 'sepa'; export type documentPaymentMethods = 'oxxo' | 'qpay' | 'boleto' | 'ideal'; @@ -13,72 +18,52 @@ export interface CustomValidationSchemaOptions { } const checkoutComShemas: { - [key in checkoutcomPaymentMethods]: (language: LanguageService) => any + [key in checkoutcomPaymentMethods]: (language: LanguageService) => any; } = { - oxxo: (language: LanguageService) => ({ ccDocument: string() .required(language.translate('payment.checkoutcom_document_invalid_error_oxxo')) - .length( - 18, - language.translate('payment.checkoutcom_document_invalid_error_oxxo') - ), + .length(18, language.translate('payment.checkoutcom_document_invalid_error_oxxo')), }), qpay: (language: LanguageService) => ({ ccDocument: string() .notRequired() - .max( - 32, - language.translate('payment.checkoutcom_document_invalid_error_qpay') - ), + .max(32, language.translate('payment.checkoutcom_document_invalid_error_qpay')), }), boleto: (language: LanguageService) => ({ ccDocument: string() .required(language.translate('payment.checkoutcom_document_invalid_error_boleto')) - .min( - 11, - language.translate('payment.checkoutcom_document_invalid_error_boleto') - ) - .max( - 14, - language.translate('payment.checkoutcom_document_invalid_error_boleto') - ), + .min(11, language.translate('payment.checkoutcom_document_invalid_error_boleto')) + .max(14, language.translate('payment.checkoutcom_document_invalid_error_boleto')), }), sepa: (language: LanguageService) => ({ - iban: string() - .required( - language.translate('payment.sepa_account_number_required') - ), - sepaMandate: boolean() - .required( - language.translate('payment.sepa_mandate_required') - ), + iban: string().required(language.translate('payment.sepa_account_number_required')), + sepaMandate: boolean().required(language.translate('payment.sepa_mandate_required')), }), ideal: (language: LanguageService) => ({ - bic: string() - .required( - language.translate('payment.ideal_bic_required') - ), + bic: string().required(language.translate('payment.ideal_bic_required')), }), fawry: (language: LanguageService) => ({ customerMobile: string() .required(language.translate('payment.checkoutcom_fawry_customer_mobile_invalid_error')) .matches( new RegExp(`^\\d{11}$`), - language.translate('payment.checkoutcom_fawry_customer_mobile_invalid_error') + language.translate('payment.checkoutcom_fawry_customer_mobile_invalid_error'), ), customerEmail: string() .required(language.translate('payment.checkoutcom_fawry_customer_email_invalid_error')) - .email( - language.translate('payment.checkoutcom_fawry_customer_email_invalid_error') - ), + .email(language.translate('payment.checkoutcom_fawry_customer_email_invalid_error')), }), }; export default memoize(function getCheckoutcomValidationSchemas({ paymentMethod, language, -}: CustomValidationSchemaOptions): ObjectSchema { - +}: CustomValidationSchemaOptions): ObjectSchema< + | DocumentOnlyCustomFormFieldsetValues + | FawryCustomFormFieldsetValues + | IdealCustomFormFieldsetValues + | SepaCustomFormFieldsetValues +> { return object(checkoutComShemas[paymentMethod](language)); }); diff --git a/packages/core/src/app/payment/checkoutcomFieldsets/index.ts b/packages/core/src/app/payment/checkoutcomFieldsets/index.ts index 3168765352..6fe7b422be 100644 --- a/packages/core/src/app/payment/checkoutcomFieldsets/index.ts +++ b/packages/core/src/app/payment/checkoutcomFieldsets/index.ts @@ -1,2 +1,7 @@ -export { default as getCheckoutcomValidationSchemas, checkoutcomPaymentMethods, documentPaymentMethods, checkoutcomCustomPaymentMethods } from './getCheckoutcomFieldsetValidationSchemas'; +export { + default as getCheckoutcomValidationSchemas, + checkoutcomPaymentMethods, + documentPaymentMethods, + checkoutcomCustomPaymentMethods, +} from './getCheckoutcomFieldsetValidationSchemas'; export * from './CheckoutcomFormValues'; diff --git a/packages/core/src/app/payment/createPaymentFormService.ts b/packages/core/src/app/payment/createPaymentFormService.ts index bd71d41245..09b6c5fc18 100644 --- a/packages/core/src/app/payment/createPaymentFormService.ts +++ b/packages/core/src/app/payment/createPaymentFormService.ts @@ -1,6 +1,10 @@ -import { PaymentFormService, PaymentFormValues } from '@bigcommerce/checkout/payment-integration-api'; import { FormikContext } from 'formik'; +import { + PaymentFormService, + PaymentFormValues, +} from '@bigcommerce/checkout/payment-integration-api'; + import { FormContextType } from '../ui/form'; import { PaymentContextProps } from './PaymentContext'; @@ -8,26 +12,14 @@ import { PaymentContextProps } from './PaymentContext'; export default function createPaymentFormService( formikContext: FormikContext, formContext: FormContextType, - paymentContext: PaymentContextProps + paymentContext: PaymentContextProps, ): PaymentFormService { - const { - setFieldTouched, - setFieldValue, - submitForm, - validateForm, - } = formikContext; + const { setFieldTouched, setFieldValue, submitForm, validateForm } = formikContext; - const { - isSubmitted, - setSubmitted, - } = formContext; + const { isSubmitted, setSubmitted } = formContext; - const { - disableSubmit, - setSubmit, - setValidationSchema, - hidePaymentSubmitButton, - } = paymentContext; + const { disableSubmit, setSubmit, setValidationSchema, hidePaymentSubmitButton } = + paymentContext; return { disableSubmit, diff --git a/packages/core/src/app/payment/creditCard/CreditCardCodeField.tsx b/packages/core/src/app/payment/creditCard/CreditCardCodeField.tsx index e521eabf6e..12b3c5c1f3 100644 --- a/packages/core/src/app/payment/creditCard/CreditCardCodeField.tsx +++ b/packages/core/src/app/payment/creditCard/CreditCardCodeField.tsx @@ -1,5 +1,5 @@ import { FieldProps } from 'formik'; -import React, { memo, useCallback, useMemo, Fragment, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo, useCallback, useMemo } from 'react'; import { TranslatedString } from '../../locale'; import { FormField, TextInput } from '../../ui/form'; @@ -13,41 +13,46 @@ export interface CreditCardCodeFieldProps { } const CreditCardCodeField: FunctionComponent = ({ name }) => { - const renderInput = useCallback(({ field }: FieldProps) => ( - - - - - - ), []); - - const labelContent = useMemo(() => ( - - - - } - > - - - - - - ), []); - - return ; + const renderInput = useCallback( + ({ field }: FieldProps) => ( + <> + + + + + ), + [], + ); + + const labelContent = useMemo( + () => ( + <> + + + }> + + + + + + ), + [], + ); + + return ( + + ); }; export default memo(CreditCardCodeField); diff --git a/packages/core/src/app/payment/creditCard/CreditCardCodeTooltip.tsx b/packages/core/src/app/payment/creditCard/CreditCardCodeTooltip.tsx index a682a09166..f6e67bb091 100644 --- a/packages/core/src/app/payment/creditCard/CreditCardCodeTooltip.tsx +++ b/packages/core/src/app/payment/creditCard/CreditCardCodeTooltip.tsx @@ -16,11 +16,11 @@ const CreditCardCodeTooltip: FunctionComponent = () => (
    - +
    - +
    diff --git a/packages/core/src/app/payment/creditCard/CreditCardCustomerCodeField.tsx b/packages/core/src/app/payment/creditCard/CreditCardCustomerCodeField.tsx index 52ce037101..4e67e6f129 100644 --- a/packages/core/src/app/payment/creditCard/CreditCardCustomerCodeField.tsx +++ b/packages/core/src/app/payment/creditCard/CreditCardCustomerCodeField.tsx @@ -1,5 +1,5 @@ import { FieldProps } from 'formik'; -import React, { memo, useCallback, useMemo, Fragment, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo, useCallback, useMemo } from 'react'; import { TranslatedString } from '../../locale'; import { FormField, TextInput } from '../../ui/form'; @@ -8,31 +8,27 @@ export interface CreditCardCustomerCodeFieldProps { name: string; } -const CreditCardCustomerCodeField: FunctionComponent = ({ name }) => { - const renderInput = useCallback(({ field }: FieldProps) => ( - - ), []); - - const labelContent = useMemo(() => ( - - - - { ' ' } - - - - - - ), []); - - return ; +const CreditCardCustomerCodeField: FunctionComponent = ({ + name, +}) => { + const renderInput = useCallback( + ({ field }: FieldProps) => , + [], + ); + + const labelContent = useMemo( + () => ( + <> + {' '} + + + + + ), + [], + ); + + return ; }; export default memo(CreditCardCustomerCodeField); diff --git a/packages/core/src/app/payment/creditCard/CreditCardExpiryField.tsx b/packages/core/src/app/payment/creditCard/CreditCardExpiryField.tsx index 0b8e078fcc..01f153226e 100644 --- a/packages/core/src/app/payment/creditCard/CreditCardExpiryField.tsx +++ b/packages/core/src/app/payment/creditCard/CreditCardExpiryField.tsx @@ -1,8 +1,8 @@ import { memoizeOne } from '@bigcommerce/memoize'; import { FieldProps } from 'formik'; -import React, { memo, useCallback, useMemo, ChangeEvent, FunctionComponent } from 'react'; +import React, { ChangeEvent, FunctionComponent, memo, useCallback, useMemo } from 'react'; -import { withLanguage, TranslatedString, WithLanguageProps } from '../../locale'; +import { TranslatedString, withLanguage, WithLanguageProps } from '../../locale'; import { FormField, TextInput } from '../../ui/form'; import formatCreditCardExpiryDate from './formatCreditCardExpiryDate'; @@ -15,33 +15,42 @@ const CreditCardExpiryField: FunctionComponent { - const handleChange = useCallback(memoizeOne((field: FieldProps['field'], form: FieldProps['form']) => { - return (event: ChangeEvent) => { - form.setFieldValue(field.name, formatCreditCardExpiryDate(event.target.value)); - }; - }), []); - - const renderInput = useCallback(({ field, form }: FieldProps) => ( - { + return (event: ChangeEvent) => { + form.setFieldValue(field.name, formatCreditCardExpiryDate(event.target.value)); + }; + }), + [], + ); + + const renderInput = useCallback( + ({ field, form }: FieldProps) => ( + + ), + [handleChange, language], + ); + + const labelContent = useMemo( + () => , + [], + ); + + return ( + - ), [handleChange, language]); - - const labelContent = useMemo(() => ( - - ), []); - - return ; + ); }; export default memo(withLanguage(CreditCardExpiryField)); diff --git a/packages/core/src/app/payment/creditCard/CreditCardFieldset.spec.tsx b/packages/core/src/app/payment/creditCard/CreditCardFieldset.spec.tsx index 2cb556a793..952a5eed5a 100644 --- a/packages/core/src/app/payment/creditCard/CreditCardFieldset.spec.tsx +++ b/packages/core/src/app/payment/creditCard/CreditCardFieldset.spec.tsx @@ -1,9 +1,10 @@ -import { CreditCardFieldsetValues } from '@bigcommerce/checkout/payment-integration-api'; import { mount, render } from 'enzyme'; import { Formik } from 'formik'; import { noop } from 'lodash'; import React from 'react'; +import { CreditCardFieldsetValues } from '@bigcommerce/checkout/payment-integration-api'; + import { getStoreConfig } from '../../config/config.mock'; import { createLocaleContext, LocaleContext, LocaleContextType } from '../../locale'; @@ -26,67 +27,52 @@ describe('CreditCardFieldset', () => { }); it('matches snapshot', () => { - expect(render( - - - - - - )) - .toMatchSnapshot(); + expect( + render( + + + + + , + ), + ).toMatchSnapshot(); }); it('shows card code field when configured', () => { const component = mount( - - - + + + - + , ); - expect(component.find('input[name="ccCvv"]').exists()) - .toEqual(true); + expect(component.find('input[name="ccCvv"]').exists()).toBe(true); }); it('shows customer code field when configured', () => { const component = mount( - - - + + + - + , ); - expect(component.find('input[name="ccCustomerCode"]').exists()) - .toEqual(true); + expect(component.find('input[name="ccCustomerCode"]').exists()).toBe(true); }); it('does not show card code field or customer code field by default', () => { const component = mount( - - + + - + , ); - expect(component.find('input[name="ccCvv"]').exists()) - .toEqual(false); + expect(component.find('input[name="ccCvv"]').exists()).toBe(false); - expect(component.find('input[name="ccCustomerCode"]').exists()) - .toEqual(false); + expect(component.find('input[name="ccCustomerCode"]').exists()).toBe(false); }); }); diff --git a/packages/core/src/app/payment/creditCard/CreditCardFieldset.tsx b/packages/core/src/app/payment/creditCard/CreditCardFieldset.tsx index 1de3f3cc01..0c0f68f06b 100644 --- a/packages/core/src/app/payment/creditCard/CreditCardFieldset.tsx +++ b/packages/core/src/app/payment/creditCard/CreditCardFieldset.tsx @@ -1,4 +1,4 @@ -import React, { memo, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo } from 'react'; import { TranslatedString } from '../../locale'; import { Fieldset, Legend } from '../../ui/form'; @@ -34,9 +34,9 @@ const CreditCardFieldset: FunctionComponent = ({ - { shouldShowCardCodeField && } + {shouldShowCardCodeField && } - { shouldShowCustomerCodeField && } + {shouldShowCustomerCodeField && }
    ); diff --git a/packages/core/src/app/payment/creditCard/CreditCardIcon.spec.tsx b/packages/core/src/app/payment/creditCard/CreditCardIcon.spec.tsx index c1ce3b6e42..264702a180 100644 --- a/packages/core/src/app/payment/creditCard/CreditCardIcon.spec.tsx +++ b/packages/core/src/app/payment/creditCard/CreditCardIcon.spec.tsx @@ -1,97 +1,114 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { IconCardAmex, IconCardCarnet, IconCardCB, IconCardDankort, IconCardDinersClub, IconCardDiscover, IconCardElo, IconCardHipercard, IconCardJCB, IconCardMada, IconCardMaestro, IconCardMastercard, IconCardTroy, IconCardUnionPay, IconCardVisa } from '../../ui/icon'; +import { + IconCardAmex, + IconCardCarnet, + IconCardCB, + IconCardDankort, + IconCardDinersClub, + IconCardDiscover, + IconCardElo, + IconCardHipercard, + IconCardJCB, + IconCardMada, + IconCardMaestro, + IconCardMastercard, + IconCardTroy, + IconCardUnionPay, + IconCardVisa, +} from '../../ui/icon'; import CreditCardIcon from './CreditCardIcon'; describe('CreditCardIcon', () => { it('returns American Express card icon', () => { - expect(shallow().find(IconCardAmex)) - .toHaveLength(1); + expect( + shallow().find(IconCardAmex), + ).toHaveLength(1); }); it('returns Diners Club card icon', () => { - expect(shallow().find(IconCardDinersClub)) - .toHaveLength(1); + expect( + shallow().find(IconCardDinersClub), + ).toHaveLength(1); }); it('returns Discover card icon', () => { - expect(shallow().find(IconCardDiscover)) - .toHaveLength(1); + expect(shallow().find(IconCardDiscover)).toHaveLength( + 1, + ); }); it('returns JCB card icon', () => { - expect(shallow().find(IconCardJCB)) - .toHaveLength(1); + expect(shallow().find(IconCardJCB)).toHaveLength(1); }); it('returns Maestro card icon', () => { - expect(shallow().find(IconCardMaestro)) - .toHaveLength(1); + expect(shallow().find(IconCardMaestro)).toHaveLength( + 1, + ); }); it('returns Mastercard card icon', () => { - expect(shallow().find(IconCardMastercard)) - .toHaveLength(1); + expect( + shallow().find(IconCardMastercard), + ).toHaveLength(1); }); it('returns Union Pay card icon', () => { - expect(shallow().find(IconCardUnionPay)) - .toHaveLength(1); + expect(shallow().find(IconCardUnionPay)).toHaveLength( + 1, + ); }); it('returns Visa card icon', () => { - expect(shallow().find(IconCardVisa)) - .toHaveLength(1); + expect(shallow().find(IconCardVisa)).toHaveLength(1); }); it('returns CB card icon', () => { - expect(shallow().find(IconCardCB)) - .toHaveLength(1); + expect(shallow().find(IconCardCB)).toHaveLength(1); }); it('returns Mada card icon', () => { - expect(shallow().find(IconCardMada)) - .toHaveLength(1); + expect(shallow().find(IconCardMada)).toHaveLength(1); }); it('returns Dankor card icon', () => { - expect(shallow().find(IconCardDankort)) - .toHaveLength(1); + expect(shallow().find(IconCardDankort)).toHaveLength( + 1, + ); }); it('returns Carnet card icon', () => { - expect(shallow().find(IconCardCarnet)) - .toHaveLength(1); + expect(shallow().find(IconCardCarnet)).toHaveLength(1); }); it('returns Elo card icon', () => { - expect(shallow().find(IconCardElo)) - .toHaveLength(1); + expect(shallow().find(IconCardElo)).toHaveLength(1); }); it('returns Hipercard card icon', () => { - expect(shallow().find(IconCardHipercard)) - .toHaveLength(1); + expect(shallow().find(IconCardHipercard)).toHaveLength( + 1, + ); }); it('returns Troy card icon', () => { - expect(shallow().find(IconCardTroy)) - .toHaveLength(1); + expect(shallow().find(IconCardTroy)).toHaveLength(1); }); it('returns default card icon if nothing matches', () => { - expect(shallow().hasClass('cardIcon-icon--default')) - .toEqual(true); + expect( + shallow().hasClass('cardIcon-icon--default'), + ).toBe(true); }); it('configures icon component with test ID', () => { - const component = shallow( - - ); + const component = shallow(); - expect(component.find(IconCardAmex).prop('testId')) - .toEqual('credit-card-icon-american-express'); + expect(component.find(IconCardAmex).prop('testId')).toBe( + 'credit-card-icon-american-express', + ); }); }); diff --git a/packages/core/src/app/payment/creditCard/CreditCardIcon.tsx b/packages/core/src/app/payment/creditCard/CreditCardIcon.tsx index aea1a43f80..d38aea3a86 100644 --- a/packages/core/src/app/payment/creditCard/CreditCardIcon.tsx +++ b/packages/core/src/app/payment/creditCard/CreditCardIcon.tsx @@ -1,4 +1,4 @@ -import React, { memo, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo } from 'react'; import { IconSize } from '../../ui/icon'; @@ -8,9 +8,7 @@ export interface CreditCardIconProps { cardType?: string; } -const CreditCardIcon: FunctionComponent = ({ - cardType, -}) => { +const CreditCardIcon: FunctionComponent = ({ cardType }) => { const iconProps = { additionalClassName: 'cardIcon-icon', size: IconSize.Medium, @@ -19,7 +17,11 @@ const CreditCardIcon: FunctionComponent = ({ const IconComponent = getPaymentMethodIconComponent(cardType); - return IconComponent ? :
    ; + return IconComponent ? ( + + ) : ( +
    + ); }; export default memo(CreditCardIcon); diff --git a/packages/core/src/app/payment/creditCard/CreditCardIconList.spec.tsx b/packages/core/src/app/payment/creditCard/CreditCardIconList.spec.tsx index 63bb0bb928..0b4af0f135 100644 --- a/packages/core/src/app/payment/creditCard/CreditCardIconList.spec.tsx +++ b/packages/core/src/app/payment/creditCard/CreditCardIconList.spec.tsx @@ -6,27 +6,19 @@ import CreditCardIconList from './CreditCardIconList'; describe('CreditCardIconList', () => { it('filters out card types without icon', () => { - const component = shallow( - - ); + const component = shallow(); - expect(component.find(CreditCardIcon)) - .toHaveLength(2); + expect(component.find(CreditCardIcon)).toHaveLength(2); - expect(component.find(CreditCardIcon).at(0).prop('cardType')) - .toEqual('visa'); + expect(component.find(CreditCardIcon).at(0).prop('cardType')).toBe('visa'); - expect(component.find(CreditCardIcon).at(1).prop('cardType')) - .toEqual('mastercard'); + expect(component.find(CreditCardIcon).at(1).prop('cardType')).toBe('mastercard'); }); it('renders nothing if no cards have icon', () => { - const component = shallow( - - ); + const component = shallow(); - expect(component.html()) - .toEqual(null); + expect(component.html()).toBeNull(); }); describe('when a credit card is selected', () => { @@ -35,28 +27,30 @@ describe('CreditCardIconList', () => { beforeEach(() => { component = shallow( + cardTypes={['visa', 'mastercard', 'foo', 'diners-club']} + selectedCardType="mastercard" + />, ); }); it('renders all supported cards', () => { - expect(component.find(CreditCardIcon)) - .toHaveLength(3); + expect(component.find(CreditCardIcon)).toHaveLength(3); }); it('applies active class to selected card', () => { - expect(component.find('.creditCardTypes-list-item').at(1).prop('className')) - .toMatch('is-active'); + expect(component.find('.creditCardTypes-list-item').at(1).prop('className')).toMatch( + 'is-active', + ); }); it('applies inactive class to unselected cards', () => { - expect(component.find('.creditCardTypes-list-item').at(0).prop('className')) - .toMatch('not-active'); + expect(component.find('.creditCardTypes-list-item').at(0).prop('className')).toMatch( + 'not-active', + ); - expect(component.find('.creditCardTypes-list-item').at(2).prop('className')) - .toMatch('not-active'); + expect(component.find('.creditCardTypes-list-item').at(2).prop('className')).toMatch( + 'not-active', + ); }); }); }); diff --git a/packages/core/src/app/payment/creditCard/CreditCardIconList.tsx b/packages/core/src/app/payment/creditCard/CreditCardIconList.tsx index 3e34a8bf8a..134844becf 100644 --- a/packages/core/src/app/payment/creditCard/CreditCardIconList.tsx +++ b/packages/core/src/app/payment/creditCard/CreditCardIconList.tsx @@ -1,8 +1,8 @@ import classNames from 'classnames'; -import React, { memo, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo } from 'react'; -import { filterInstrumentTypes } from './mapFromPaymentMethodCardType'; import CreditCardIcon from './CreditCardIcon'; +import { filterInstrumentTypes } from './mapFromPaymentMethodCardType'; export interface CreditCardIconListProps { selectedCardType?: string; @@ -21,20 +21,20 @@ const CreditCardIconList: FunctionComponent = ({ return (
      - { filteredCardTypes.map(type => ( + {filteredCardTypes.map((type) => (
    • - +
    • - )) } + ))}
    ); }; diff --git a/packages/core/src/app/payment/creditCard/CreditCardNameField.tsx b/packages/core/src/app/payment/creditCard/CreditCardNameField.tsx index 2db5936201..813f732fbf 100644 --- a/packages/core/src/app/payment/creditCard/CreditCardNameField.tsx +++ b/packages/core/src/app/payment/creditCard/CreditCardNameField.tsx @@ -1,4 +1,4 @@ -import React, { memo, useCallback, useMemo, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo, useCallback, useMemo } from 'react'; import { TranslatedString } from '../../locale'; import { FormField, TextInput } from '../../ui/form'; @@ -8,24 +8,24 @@ export interface CreditCardNameFieldProps { } const CreditCardNameField: FunctionComponent = ({ name }) => { - const renderInput = useCallback(({ field }) => ( - - ), []); + const renderInput = useCallback( + ({ field }) => , + [], + ); - const labelContent = useMemo(() => ( - - ), []); + const labelContent = useMemo( + () => , + [], + ); - return ; + return ( + + ); }; export default memo(CreditCardNameField); diff --git a/packages/core/src/app/payment/creditCard/CreditCardNumberField.spec.tsx b/packages/core/src/app/payment/creditCard/CreditCardNumberField.spec.tsx index 6a4997b0d9..47aec5307c 100644 --- a/packages/core/src/app/payment/creditCard/CreditCardNumberField.spec.tsx +++ b/packages/core/src/app/payment/creditCard/CreditCardNumberField.spec.tsx @@ -1,9 +1,10 @@ -import { CreditCardFieldsetValues } from '@bigcommerce/checkout/payment-integration-api'; import { mount } from 'enzyme'; import { Formik } from 'formik'; import { noop } from 'lodash'; import React from 'react'; +import { CreditCardFieldsetValues } from '@bigcommerce/checkout/payment-integration-api'; + import { getStoreConfig } from '../../config/config.mock'; import { createLocaleContext, LocaleContext, LocaleContextType } from '../../locale'; @@ -23,111 +24,98 @@ describe('CreditCardNumberField', () => { it('allows user to type in potentially valid number', () => { const component = mount( - - + + - + , ); - component.find('input[name="ccNumber"]') + component + .find('input[name="ccNumber"]') .simulate('change', { target: { value: '4111', name: 'ccNumber' } }) .update(); - expect(component.find('input[name="ccNumber"]').prop('value')) - .toEqual('4111'); + expect(component.find('input[name="ccNumber"]').prop('value')).toBe('4111'); }); it('prevents user from typing invalid number', () => { const component = mount( - - + + - + , ); - component.find('input[name="ccNumber"]') + component + .find('input[name="ccNumber"]') .simulate('change', { target: { value: 'xxxx', name: 'ccNumber' } }) .update(); - expect(component.find('input[name="ccNumber"]').prop('value')) - .toEqual(''); + expect(component.find('input[name="ccNumber"]').prop('value')).toBe(''); }); it('limits number of digits based on card type', () => { const component = mount( - - + + - + , ); // The number of digits for Visa card should not exceed 19 digits (plus 3 gaps) - component.find('input[name="ccNumber"]') - .simulate('change', { target: { value: '4111 1111 1111 1111111 999999', name: 'ccNumber' } }) + component + .find('input[name="ccNumber"]') + .simulate('change', { + target: { value: '4111 1111 1111 1111111 999999', name: 'ccNumber' }, + }) .update(); - expect(component.find('input[name="ccNumber"]').prop('value')) - .toHaveLength(22); + expect(component.find('input[name="ccNumber"]').prop('value')).toHaveLength(22); // The number of digits for Amex card should not exceed 15 digits (plus 2 gaps) - component.find('input[name="ccNumber"]') + component + .find('input[name="ccNumber"]') .simulate('change', { target: { value: '3782 822463 10005 999999', name: 'ccNumber' } }) .update(); - expect(component.find('input[name="ccNumber"]').prop('value')) - .toHaveLength(17); + expect(component.find('input[name="ccNumber"]').prop('value')).toHaveLength(17); }); it('formats card number based on type', () => { const component = mount( - - + + - + , ); - component.find('input[name="ccNumber"]') + component + .find('input[name="ccNumber"]') .simulate('change', { target: { value: '411111111111', name: 'ccNumber' } }) .update(); - expect(component.find('input[name="ccNumber"]').prop('value')) - .toEqual('4111 1111 1111'); + expect(component.find('input[name="ccNumber"]').prop('value')).toBe('4111 1111 1111'); - component.find('input[name="ccNumber"]') + component + .find('input[name="ccNumber"]') .simulate('change', { target: { value: '378282246310005', name: 'ccNumber' } }) .update(); - expect(component.find('input[name="ccNumber"]').prop('value')) - .toEqual('3782 822463 10005'); + expect(component.find('input[name="ccNumber"]').prop('value')).toBe('3782 822463 10005'); }); it('only sets selection range if it has changed', () => { const ccNumber = '4111'; const component = mount( - - + + - + , ); const input = component.find('input[name="ccNumber"]'); @@ -137,9 +125,7 @@ describe('CreditCardNumberField', () => { jest.spyOn(inputNode, 'setSelectionRange'); // Trigger a change event with the same value - input - .simulate('change', { target: { value: ccNumber, name: 'ccNumber' } }) - .update(); + input.simulate('change', { target: { value: ccNumber, name: 'ccNumber' } }).update(); expect(inputNode.setSelectionRange).toHaveBeenCalledTimes(0); }); diff --git a/packages/core/src/app/payment/creditCard/CreditCardNumberField.tsx b/packages/core/src/app/payment/creditCard/CreditCardNumberField.tsx index 715acd38a7..a569951c9b 100644 --- a/packages/core/src/app/payment/creditCard/CreditCardNumberField.tsx +++ b/packages/core/src/app/payment/creditCard/CreditCardNumberField.tsx @@ -1,7 +1,17 @@ import creditCardType from 'credit-card-type'; import { FieldProps } from 'formik'; import { max } from 'lodash'; -import React, { createRef, memo, useCallback, useMemo, ChangeEventHandler, Fragment, FunctionComponent, PureComponent, ReactNode, RefObject } from 'react'; +import React, { + ChangeEventHandler, + createRef, + FunctionComponent, + memo, + PureComponent, + ReactNode, + RefObject, + useCallback, + useMemo, +} from 'react'; import { TranslatedString } from '../../locale'; import { FormField, TextInput } from '../../ui/form'; @@ -14,28 +24,31 @@ export interface CreditCardNumberFieldProps { } const CreditCardNumberField: FunctionComponent = ({ name }) => { - const renderInput = useCallback(({ field, form }: FieldProps) => ( - ) => ( + + ), + [], + ); + + const labelContent = useMemo( + () => , + [], + ); + + return ( + - ), []); - - const labelContent = useMemo(() => ( - - ), []); - - return ; + ); }; class CreditCardNumberInput extends PureComponent> { private inputRef: RefObject = createRef(); - private nextSelectionEnd: number = 0; + private nextSelectionEnd = 0; componentDidUpdate(): void { if (this.inputRef.current && this.inputRef.current.selectionEnd !== this.nextSelectionEnd) { @@ -47,23 +60,23 @@ class CreditCardNumberInput extends PureComponent> { const { field } = this.props; return ( - + <> - + ); } - private handleChange: ChangeEventHandler = event => { + private handleChange: ChangeEventHandler = (event) => { const separator = ' '; const { value = '' } = event.target; const { field, form } = this.props; @@ -75,14 +88,11 @@ class CreditCardNumberInput extends PureComponent> { return form.setFieldValue(name, previousValue); } - const maxLength = max( - creditCardType(value) - .map(info => max(info.lengths)) - ); + const maxLength = max(creditCardType(value).map((info) => max(info.lengths))); const formattedValue = formatCreditCardNumber( value.replace(new RegExp(separator, 'g'), '').slice(0, maxLength), - separator + separator, ); if (selectionEnd === value.length && value.length < formattedValue.length) { diff --git a/packages/core/src/app/payment/creditCard/TextFieldForm.spec.tsx b/packages/core/src/app/payment/creditCard/TextFieldForm.spec.tsx index 16dd20cddd..caa8013cab 100644 --- a/packages/core/src/app/payment/creditCard/TextFieldForm.spec.tsx +++ b/packages/core/src/app/payment/creditCard/TextFieldForm.spec.tsx @@ -1,9 +1,9 @@ -import { DocumentOnlyCustomFormFieldsetValues } from '@bigcommerce/checkout/payment-integration-api'; import { mount } from 'enzyme'; import { Formik } from 'formik'; import { noop } from 'lodash'; import React, { FunctionComponent } from 'react'; +import { DocumentOnlyCustomFormFieldsetValues } from '@bigcommerce/checkout/payment-integration-api'; import TextFieldForm, { TextFieldFormProps } from './TextFieldForm'; @@ -22,20 +22,16 @@ describe('TextFieldForm', () => { initialValues = { ccDocument: '' }; - TextFieldFormTest = props => ( - - + TextFieldFormTest = (props) => ( + + ); }); it('renders text field with provided name', () => { - const container = mount(); + const container = mount(); - expect(container.find('input[id="custom-name"]').exists()) - .toEqual(true); + expect(container.find('input[id="custom-name"]').exists()).toBe(true); }); }); diff --git a/packages/core/src/app/payment/creditCard/TextFieldForm.tsx b/packages/core/src/app/payment/creditCard/TextFieldForm.tsx index de56f8839f..346ef5da01 100644 --- a/packages/core/src/app/payment/creditCard/TextFieldForm.tsx +++ b/packages/core/src/app/payment/creditCard/TextFieldForm.tsx @@ -1,4 +1,4 @@ -import React, { memo, useCallback, useMemo, FunctionComponent } from 'react'; +import React, { FunctionComponent, memo, useCallback, useMemo } from 'react'; import { TranslatedString } from '../../locale'; import { Fieldset, FormField, TextInput } from '../../ui/form'; @@ -17,27 +17,24 @@ export interface TextFieldFormProps { name: string; } -const TextField: FunctionComponent = props => { +const TextField: FunctionComponent = (props) => { const { additionalClassName, autoComplete, labelId, name } = props; - const renderInput = useCallback(({ field }) => ( - , + [autoComplete], + ); + + const labelContent = useMemo(() => , [labelId]); + + return ( + - ), [autoComplete]); - - const labelContent = useMemo(() => ( - - ), [labelId]); - - return ; + ); }; const TextFieldForm: FunctionComponent = ({ @@ -49,10 +46,10 @@ const TextFieldForm: FunctionComponent = ({
    diff --git a/packages/core/src/app/payment/creditCard/configureCardValidator.ts b/packages/core/src/app/payment/creditCard/configureCardValidator.ts index 7cb4ba5791..6773a6c4e5 100644 --- a/packages/core/src/app/payment/creditCard/configureCardValidator.ts +++ b/packages/core/src/app/payment/creditCard/configureCardValidator.ts @@ -11,10 +11,7 @@ export default function configureCardValidator(): void { // Add support for 8-BIN Discover Cards. creditCardType.updateCard('discover', { - patterns: [ - ...(discoverInfo.patterns || []), - [810, 817], - ], + patterns: [...(discoverInfo.patterns || []), [810, 817]], }); // Add support for Mada-BIN Cards. @@ -22,26 +19,15 @@ export default function configureCardValidator(): void { niceType: 'Mada', type: 'mada', patterns: [ - 400861, 401757, 407197, 407395, 409201, - 410685, 412565, 417633, 419593, 422817, - 422818, 422819, 428331, 428671, 428672, - 428673, 431361, 432328, 434107, 439954, - 440533, 440647, 440795, 445564, 446393, - 446404, 446672, 455036, 455708, 457865, - 458456, 462220, 468540, 468541, 468542, - 468543, 483010, 483011, 483012, 484783, - 486094, 486095, 486096, 489317, 489318, - 489319, 493428, 504300, 506968, 508160, - 513213, 520058, 521076, 524130, 524514, - 529415, 529741, 530060, 530906, 531095, - 531196, 532013, 535825, 535989, 536023, - 537767, 539931, 543085, 543357, 549760, - 554180, 557606, 558848, 585265, 588845, - 588846, 588847, 588848, 588849, 588850, - 588851, 588982, 588983, 589005, 589206, - 604906, 605141, 636120, 968201, 968202, - 968203, 968204, 968205, 968206, 968207, - 968208, 968209, 968210, 968211, + 400861, 401757, 407197, 407395, 409201, 410685, 412565, 417633, 419593, 422817, 422818, + 422819, 428331, 428671, 428672, 428673, 431361, 432328, 434107, 439954, 440533, 440647, + 440795, 445564, 446393, 446404, 446672, 455036, 455708, 457865, 458456, 462220, 468540, + 468541, 468542, 468543, 483010, 483011, 483012, 484783, 486094, 486095, 486096, 489317, + 489318, 489319, 493428, 504300, 506968, 508160, 513213, 520058, 521076, 524130, 524514, + 529415, 529741, 530060, 530906, 531095, 531196, 532013, 535825, 535989, 536023, 537767, + 539931, 543085, 543357, 549760, 554180, 557606, 558848, 585265, 588845, 588846, 588847, + 588848, 588849, 588850, 588851, 588982, 588983, 589005, 589206, 604906, 605141, 636120, + 968201, 968202, 968203, 968204, 968205, 968206, 968207, 968208, 968209, 968210, 968211, ], gaps: [4, 8, 12], lengths: [16, 18, 19], diff --git a/packages/core/src/app/payment/creditCard/formatCreditCardExpiryDate.spec.ts b/packages/core/src/app/payment/creditCard/formatCreditCardExpiryDate.spec.ts index fd085dee9f..db2c43930e 100644 --- a/packages/core/src/app/payment/creditCard/formatCreditCardExpiryDate.spec.ts +++ b/packages/core/src/app/payment/creditCard/formatCreditCardExpiryDate.spec.ts @@ -2,22 +2,18 @@ import formatCreditCardExpiryDate from './formatCreditCardExpiryDate'; describe('formatCreditCardExpiryDate()', () => { it('converts date into MM/YY date format', () => { - expect(formatCreditCardExpiryDate('10/2019')) - .toEqual('10 / 19'); + expect(formatCreditCardExpiryDate('10/2019')).toBe('10 / 19'); }); it('formats partial date value', () => { - expect(formatCreditCardExpiryDate('10')) - .toEqual('10 / '); + expect(formatCreditCardExpiryDate('10')).toBe('10 / '); }); it('returns month only if there is no year and separator has no trailing space', () => { - expect(formatCreditCardExpiryDate('10 /')) - .toEqual('10'); + expect(formatCreditCardExpiryDate('10 /')).toBe('10'); }); it('surrounds separator with whitespaces', () => { - expect(formatCreditCardExpiryDate('10/19')) - .toEqual('10 / 19'); + expect(formatCreditCardExpiryDate('10/19')).toBe('10 / 19'); }); }); diff --git a/packages/core/src/app/payment/creditCard/formatCreditCardExpiryDate.ts b/packages/core/src/app/payment/creditCard/formatCreditCardExpiryDate.ts index 9322099de7..1fe3eba83b 100644 --- a/packages/core/src/app/payment/creditCard/formatCreditCardExpiryDate.ts +++ b/packages/core/src/app/payment/creditCard/formatCreditCardExpiryDate.ts @@ -2,7 +2,8 @@ export default function formatCreditCardExpiryDate(value: string): string { const separator = '/'; const [month = '', year = ''] = value.split(new RegExp(`\\s*${separator}\\s*`)); const trimmedMonth = month.slice(0, 2); - const trimmedYear = year.length === 4 ? year.slice(-2) : (year ? year.slice(0, 2) : month.slice(2)); + const trimmedYear = + year.length === 4 ? year.slice(-2) : year ? year.slice(0, 2) : month.slice(2); // i.e.: '1' if (value.length < 2) { diff --git a/packages/core/src/app/payment/creditCard/formatCreditCardNumber.spec.ts b/packages/core/src/app/payment/creditCard/formatCreditCardNumber.spec.ts index 1742a9960f..54582281df 100644 --- a/packages/core/src/app/payment/creditCard/formatCreditCardNumber.spec.ts +++ b/packages/core/src/app/payment/creditCard/formatCreditCardNumber.spec.ts @@ -2,49 +2,38 @@ import formatCreditCardNumber from './formatCreditCardNumber'; describe('formatCreditCardNumber()', () => { it('formats Visa credit card number', () => { - expect(formatCreditCardNumber('4111111111111111')) - .toEqual('4111 1111 1111 1111'); + expect(formatCreditCardNumber('4111111111111111')).toBe('4111 1111 1111 1111'); - expect(formatCreditCardNumber('4111 1111 1111 1111234')) - .toEqual('4111 1111 1111 1111234'); + expect(formatCreditCardNumber('4111 1111 1111 1111234')).toBe('4111 1111 1111 1111234'); }); it('formats Mastercard credit card number', () => { - expect(formatCreditCardNumber('5555555555554444')) - .toEqual('5555 5555 5555 4444'); + expect(formatCreditCardNumber('5555555555554444')).toBe('5555 5555 5555 4444'); }); it('formats Amex credit card number', () => { - expect(formatCreditCardNumber('378282246310005')) - .toEqual('3782 822463 10005'); + expect(formatCreditCardNumber('378282246310005')).toBe('3782 822463 10005'); }); it('formats Diners Club credit card number', () => { - expect(formatCreditCardNumber('36259600000004')) - .toEqual('3625 960000 0004'); + expect(formatCreditCardNumber('36259600000004')).toBe('3625 960000 0004'); }); it('formats Discover credit card number', () => { - expect(formatCreditCardNumber('6011111111111117')) - .toEqual('6011 1111 1111 1117'); + expect(formatCreditCardNumber('6011111111111117')).toBe('6011 1111 1111 1117'); }); it('formats potentially invalid credit card number', () => { - expect(formatCreditCardNumber('41111')) - .toEqual('4111 1'); + expect(formatCreditCardNumber('41111')).toBe('4111 1'); - expect(formatCreditCardNumber('5555')) - .toEqual('5555'); + expect(formatCreditCardNumber('5555')).toBe('5555'); - expect(formatCreditCardNumber('37828224631')) - .toEqual('3782 822463 1'); + expect(formatCreditCardNumber('37828224631')).toBe('3782 822463 1'); }); it('does not format if credit card number cannot be recognized', () => { - expect(formatCreditCardNumber('99999999')) - .toEqual('99999999'); + expect(formatCreditCardNumber('99999999')).toBe('99999999'); - expect(formatCreditCardNumber('4111 1111 1111 11112345')) - .toEqual('4111 1111 1111 11112345'); + expect(formatCreditCardNumber('4111 1111 1111 11112345')).toBe('4111 1111 1111 11112345'); }); }); diff --git a/packages/core/src/app/payment/creditCard/formatCreditCardNumber.ts b/packages/core/src/app/payment/creditCard/formatCreditCardNumber.ts index 4bec9576ea..7fd043887b 100644 --- a/packages/core/src/app/payment/creditCard/formatCreditCardNumber.ts +++ b/packages/core/src/app/payment/creditCard/formatCreditCardNumber.ts @@ -2,7 +2,7 @@ import { number } from 'card-validator'; import unformatCreditCardNumber from './unformatCreditCardNumber'; -export default function formatCreditCardNumber(value: string, separator: string = ' '): string { +export default function formatCreditCardNumber(value: string, separator = ' '): string { const { card } = number(value); if (!card) { @@ -12,11 +12,10 @@ export default function formatCreditCardNumber(value: string, separator: string const unformattedValue = unformatCreditCardNumber(value, separator); return card.gaps - .filter(gapIndex => unformattedValue.length > gapIndex) - .reduce((output, gapIndex, index) => ( - [ - output.slice(0, gapIndex + index), - output.slice(gapIndex + index), - ].join(separator) - ), unformattedValue); + .filter((gapIndex) => unformattedValue.length > gapIndex) + .reduce( + (output, gapIndex, index) => + [output.slice(0, gapIndex + index), output.slice(gapIndex + index)].join(separator), + unformattedValue, + ); } diff --git a/packages/core/src/app/payment/creditCard/getCreditCardInputStyles.spec.tsx b/packages/core/src/app/payment/creditCard/getCreditCardInputStyles.spec.tsx index 16f5eadda3..b5ca387ef5 100644 --- a/packages/core/src/app/payment/creditCard/getCreditCardInputStyles.spec.tsx +++ b/packages/core/src/app/payment/creditCard/getCreditCardInputStyles.spec.tsx @@ -40,30 +40,35 @@ describe('getCreditCardInputStyles', () => { it('returns default styles of credit card input', async () => { const styles = await getCreditCardInputStyles('form-field', ['color', 'fontSize']); - expect(styles) - .toEqual({ - color: 'rgb(0, 0, 0)', - fontSize: '20px', - }); + expect(styles).toEqual({ + color: 'rgb(0, 0, 0)', + fontSize: '20px', + }); }); it('returns error styles of credit card input', async () => { - const styles = await getCreditCardInputStyles('form-field', ['color', 'fontSize'], CreditCardInputStylesType.Error); + const styles = await getCreditCardInputStyles( + 'form-field', + ['color', 'fontSize'], + CreditCardInputStylesType.Error, + ); - expect(styles) - .toEqual({ - color: 'rgb(255, 0, 0)', - fontSize: '20px', - }); + expect(styles).toEqual({ + color: 'rgb(255, 0, 0)', + fontSize: '20px', + }); }); it('returns focus styles of credit card input', async () => { - const styles = await getCreditCardInputStyles('form-field', ['color', 'fontSize'], CreditCardInputStylesType.Focus); + const styles = await getCreditCardInputStyles( + 'form-field', + ['color', 'fontSize'], + CreditCardInputStylesType.Focus, + ); - expect(styles) - .toEqual({ - color: 'rgb(0, 0, 255)', - fontSize: '20px', - }); + expect(styles).toEqual({ + color: 'rgb(0, 0, 255)', + fontSize: '20px', + }); }); }); diff --git a/packages/core/src/app/payment/creditCard/getCreditCardInputStyles.tsx b/packages/core/src/app/payment/creditCard/getCreditCardInputStyles.tsx index b2103a4b11..b07e1c32f1 100644 --- a/packages/core/src/app/payment/creditCard/getCreditCardInputStyles.tsx +++ b/packages/core/src/app/payment/creditCard/getCreditCardInputStyles.tsx @@ -14,18 +14,20 @@ export enum CreditCardInputStylesType { export default function getCreditCardInputStyles( containerId: string, properties: string[], - type: CreditCardInputStylesType = CreditCardInputStylesType.Default + type: CreditCardInputStylesType = CreditCardInputStylesType.Default, ): Promise<{ [key: string]: string }> { const container = document.createElement('div'); const parentContainer = document.getElementById(containerId); if (!parentContainer) { - throw new Error('Unable to retrieve input styles as the provided container ID is not valid.'); + throw new Error( + 'Unable to retrieve input styles as the provided container ID is not valid.', + ); } parentContainer.appendChild(container); - return new Promise(resolve => { + return new Promise((resolve) => { const callbackRef = (element: HTMLInputElement | null) => { if (!element) { return; @@ -41,15 +43,15 @@ export default function getCreditCardInputStyles( }; ReactDOM.render( - - + + , - container + container, ); }); } diff --git a/packages/core/src/app/payment/creditCard/getCreditCardValidationSchema.spec.ts b/packages/core/src/app/payment/creditCard/getCreditCardValidationSchema.spec.ts index c2314fd9c3..fa93e1f2f1 100644 --- a/packages/core/src/app/payment/creditCard/getCreditCardValidationSchema.spec.ts +++ b/packages/core/src/app/payment/creditCard/getCreditCardValidationSchema.spec.ts @@ -1,6 +1,7 @@ -import { CreditCardFieldsetValues } from '@bigcommerce/checkout/payment-integration-api'; import { createLanguageService, LanguageService } from '@bigcommerce/checkout-sdk'; +import { CreditCardFieldsetValues } from '@bigcommerce/checkout/payment-integration-api'; + import getCreditCardValidationSchema from './getCreditCardValidationSchema'; describe('getCreditCardValidationSchema()', () => { @@ -10,8 +11,7 @@ describe('getCreditCardValidationSchema()', () => { beforeEach(() => { language = createLanguageService(); - jest.spyOn(language, 'translate') - .mockImplementation(key => key); + jest.spyOn(language, 'translate').mockImplementation((key) => key); validData = { ccCustomerCode: '123', @@ -25,78 +25,85 @@ describe('getCreditCardValidationSchema()', () => { it('does not throw error if data is valid', () => { const schema = getCreditCardValidationSchema({ isCardCodeRequired: false, language }); - expect(schema.validateSync(validData)) - .toEqual(validData); + expect(schema.validateSync(validData)).toEqual(validData); }); it('returns error if card number is missing', () => { const schema = getCreditCardValidationSchema({ isCardCodeRequired: false, language }); - expect(() => schema.validateSync({ ...validData, ccNumber: '' })) - .toThrowError('payment.credit_card_number_required_error'); + expect(() => schema.validateSync({ ...validData, ccNumber: '' })).toThrow( + 'payment.credit_card_number_required_error', + ); }); it('returns error if card number is invalid', () => { const schema = getCreditCardValidationSchema({ isCardCodeRequired: false, language }); - expect(() => schema.validateSync({ ...validData, ccNumber: '9999 9999 9999 9999' })) - .toThrowError('payment.credit_card_number_invalid_error'); + expect(() => + schema.validateSync({ ...validData, ccNumber: '9999 9999 9999 9999' }), + ).toThrow('payment.credit_card_number_invalid_error'); }); it('returns error if card name is missing', () => { const schema = getCreditCardValidationSchema({ isCardCodeRequired: false, language }); - expect(() => schema.validateSync({ ...validData, ccName: '' })) - .toThrowError('payment.credit_card_name_required_error'); + expect(() => schema.validateSync({ ...validData, ccName: '' })).toThrow( + 'payment.credit_card_name_required_error', + ); }); it('returns error if expiry date is missing', () => { const schema = getCreditCardValidationSchema({ isCardCodeRequired: false, language }); - expect(() => schema.validateSync({ ...validData, ccExpiry: '' })) - .toThrowError('payment.credit_card_expiration_required_error'); + expect(() => schema.validateSync({ ...validData, ccExpiry: '' })).toThrow( + 'payment.credit_card_expiration_required_error', + ); }); it('returns error if expiry date is invalid', () => { const schema = getCreditCardValidationSchema({ isCardCodeRequired: false, language }); - expect(() => schema.validateSync({ ...validData, ccExpiry: '2030 / 12' })) - .toThrowError('payment.credit_card_expiration_invalid_error'); + expect(() => schema.validateSync({ ...validData, ccExpiry: '2030 / 12' })).toThrow( + 'payment.credit_card_expiration_invalid_error', + ); }); it('returns error if expiry date is in past', () => { const schema = getCreditCardValidationSchema({ isCardCodeRequired: false, language }); - expect(() => schema.validateSync({ ...validData, ccExpiry: '12 / 10' })) - .toThrowError('payment.credit_card_expiration_invalid_error'); + expect(() => schema.validateSync({ ...validData, ccExpiry: '12 / 10' })).toThrow( + 'payment.credit_card_expiration_invalid_error', + ); }); it('returns error if card code is missing when required', () => { const schema = getCreditCardValidationSchema({ isCardCodeRequired: true, language }); - expect(() => schema.validateSync({ ...validData, ccCvv: '' })) - .toThrowError('payment.credit_card_cvv_required_error'); + expect(() => schema.validateSync({ ...validData, ccCvv: '' })).toThrow( + 'payment.credit_card_cvv_required_error', + ); }); it('returns error if card code is invalid when required', () => { const schema = getCreditCardValidationSchema({ isCardCodeRequired: true, language }); - expect(() => schema.validateSync({ ...validData, ccCvv: '99999' })) - .toThrowError('payment.credit_card_cvv_invalid_error'); + expect(() => schema.validateSync({ ...validData, ccCvv: '99999' })).toThrow( + 'payment.credit_card_cvv_invalid_error', + ); }); it('returns error if card code is invalid for given card number', () => { const schema = getCreditCardValidationSchema({ isCardCodeRequired: true, language }); // Card code for American Express should have 4 digts - expect(() => schema.validateSync({ ...validData, ccCvv: '123', ccNumber: '378282246310005' })) - .toThrowError('payment.credit_card_cvv_invalid_error'); + expect(() => + schema.validateSync({ ...validData, ccCvv: '123', ccNumber: '378282246310005' }), + ).toThrow('payment.credit_card_cvv_invalid_error'); }); it('does not return error if card code is not required', () => { const schema = getCreditCardValidationSchema({ isCardCodeRequired: false, language }); - expect(() => schema.validateSync({ ...validData, ccCvv: '' })) - .not.toThrow(); + expect(() => schema.validateSync({ ...validData, ccCvv: '' })).not.toThrow(); }); }); diff --git a/packages/core/src/app/payment/creditCard/getCreditCardValidationSchema.ts b/packages/core/src/app/payment/creditCard/getCreditCardValidationSchema.ts index a9056548f3..d373e07d80 100644 --- a/packages/core/src/app/payment/creditCard/getCreditCardValidationSchema.ts +++ b/packages/core/src/app/payment/creditCard/getCreditCardValidationSchema.ts @@ -1,7 +1,7 @@ import { LanguageService } from '@bigcommerce/checkout-sdk'; import { memoize } from '@bigcommerce/memoize'; import { cvv, expirationDate, number } from 'card-validator'; -import { object, string, ObjectSchema } from 'yup'; +import { object, ObjectSchema, string } from 'yup'; import { CreditCardFieldsetValues } from '../paymentMethod'; @@ -21,7 +21,7 @@ export default memoize(function getCreditCardValidationSchema({ .required(language.translate('payment.credit_card_expiration_required_error')) .test({ message: language.translate('payment.credit_card_expiration_invalid_error'), - test: value => expirationDate(value).isValid, + test: (value) => expirationDate(value).isValid, }), ccName: string() .max(200) @@ -30,7 +30,7 @@ export default memoize(function getCreditCardValidationSchema({ .required(language.translate('payment.credit_card_number_required_error')) .test({ message: language.translate('payment.credit_card_number_invalid_error'), - test: value => number(value).isValid, + test: (value) => number(value).isValid, }), }; diff --git a/packages/core/src/app/payment/creditCard/index.ts b/packages/core/src/app/payment/creditCard/index.ts index 22e1435e65..9e83f51762 100644 --- a/packages/core/src/app/payment/creditCard/index.ts +++ b/packages/core/src/app/payment/creditCard/index.ts @@ -9,9 +9,16 @@ export { default as CreditCardCustomField } from './TextFieldForm'; export { default as CreditCardExpiryField } from './CreditCardExpiryField'; export { default as CreditCardNameField } from './CreditCardNameField'; export { default as CreditCardNumberField } from './CreditCardNumberField'; -export { default as getCreditCardInputStyles, CreditCardInputStylesType } from './getCreditCardInputStyles'; +export { + default as getCreditCardInputStyles, + CreditCardInputStylesType, +} from './getCreditCardInputStyles'; export { default as getCreditCardValidationSchema } from './getCreditCardValidationSchema'; -export { default as mapFromPaymentMethodCardType, getPaymentMethodIconComponent, filterInstrumentTypes } from './mapFromPaymentMethodCardType'; +export { + default as mapFromPaymentMethodCardType, + getPaymentMethodIconComponent, + filterInstrumentTypes, +} from './mapFromPaymentMethodCardType'; export { default as unformatCreditCardNumber } from './unformatCreditCardNumber'; export { default as unformatCreditCardExpiryDate } from './unformatCreditCardExpiryDate'; export { default as TextFieldForm } from './TextFieldForm'; diff --git a/packages/core/src/app/payment/creditCard/mapFromPaymentMethodCardType.spec.ts b/packages/core/src/app/payment/creditCard/mapFromPaymentMethodCardType.spec.ts index aad33e4bae..1fa07d5cec 100644 --- a/packages/core/src/app/payment/creditCard/mapFromPaymentMethodCardType.spec.ts +++ b/packages/core/src/app/payment/creditCard/mapFromPaymentMethodCardType.spec.ts @@ -2,54 +2,38 @@ import mapFromPaymentMethodCardType from './mapFromPaymentMethodCardType'; describe('mapFromPaymentMethodCardType()', () => { it('maps from payment method card type', () => { - expect(mapFromPaymentMethodCardType('AMEX')) - .toEqual('american-express'); + expect(mapFromPaymentMethodCardType('AMEX')).toBe('american-express'); - expect(mapFromPaymentMethodCardType('DINERS')) - .toEqual('diners-club'); + expect(mapFromPaymentMethodCardType('DINERS')).toBe('diners-club'); - expect(mapFromPaymentMethodCardType('DISCOVER')) - .toEqual('discover'); + expect(mapFromPaymentMethodCardType('DISCOVER')).toBe('discover'); - expect(mapFromPaymentMethodCardType('JCB')) - .toEqual('jcb'); + expect(mapFromPaymentMethodCardType('JCB')).toBe('jcb'); - expect(mapFromPaymentMethodCardType('MAESTRO')) - .toEqual('maestro'); + expect(mapFromPaymentMethodCardType('MAESTRO')).toBe('maestro'); - expect(mapFromPaymentMethodCardType('MC')) - .toEqual('mastercard'); + expect(mapFromPaymentMethodCardType('MC')).toBe('mastercard'); - expect(mapFromPaymentMethodCardType('CUP')) - .toEqual('unionpay'); + expect(mapFromPaymentMethodCardType('CUP')).toBe('unionpay'); - expect(mapFromPaymentMethodCardType('VISA')) - .toEqual('visa'); + expect(mapFromPaymentMethodCardType('VISA')).toBe('visa'); - expect(mapFromPaymentMethodCardType('CB')) - .toEqual('cb'); + expect(mapFromPaymentMethodCardType('CB')).toBe('cb'); - expect(mapFromPaymentMethodCardType('MADA')) - .toEqual('mada'); + expect(mapFromPaymentMethodCardType('MADA')).toBe('mada'); - expect(mapFromPaymentMethodCardType('DANKORT')) - .toEqual('dankort'); + expect(mapFromPaymentMethodCardType('DANKORT')).toBe('dankort'); - expect(mapFromPaymentMethodCardType('CARNET')) - .toEqual('carnet'); + expect(mapFromPaymentMethodCardType('CARNET')).toBe('carnet'); - expect(mapFromPaymentMethodCardType('ELO')) - .toEqual('elo'); + expect(mapFromPaymentMethodCardType('ELO')).toBe('elo'); - expect(mapFromPaymentMethodCardType('HIPER')) - .toEqual('hiper'); + expect(mapFromPaymentMethodCardType('HIPER')).toBe('hiper'); - expect(mapFromPaymentMethodCardType('TROY')) - .toEqual('troy'); + expect(mapFromPaymentMethodCardType('TROY')).toBe('troy'); }); it('returns undefined if unable to map type', () => { - expect(mapFromPaymentMethodCardType('FOOBAR')) - .toEqual(undefined); + expect(mapFromPaymentMethodCardType('FOOBAR')).toBeUndefined(); }); }); diff --git a/packages/core/src/app/payment/creditCard/mapFromPaymentMethodCardType.ts b/packages/core/src/app/payment/creditCard/mapFromPaymentMethodCardType.ts index 090c462c10..c716da371c 100644 --- a/packages/core/src/app/payment/creditCard/mapFromPaymentMethodCardType.ts +++ b/packages/core/src/app/payment/creditCard/mapFromPaymentMethodCardType.ts @@ -1,6 +1,30 @@ import { ComponentType } from 'react'; -import { IconBitCoin, IconBitCoinCash, IconCardAmex, IconCardCarnet, IconCardCB, IconCardDankort, IconCardDinersClub, IconCardDiscover, IconCardElo, IconCardHipercard, IconCardJCB, IconCardMada, IconCardMaestro, IconCardMastercard, IconCardTroy, IconCardUnionPay, IconCardVisa, IconDogeCoin, IconEthereum, IconLiteCoin, IconProps, IconShibaInu, IconUsdCoin } from '../../ui/icon'; +import { + IconBitCoin, + IconBitCoinCash, + IconCardAmex, + IconCardCarnet, + IconCardCB, + IconCardDankort, + IconCardDinersClub, + IconCardDiscover, + IconCardElo, + IconCardHipercard, + IconCardJCB, + IconCardMada, + IconCardMaestro, + IconCardMastercard, + IconCardTroy, + IconCardUnionPay, + IconCardVisa, + IconDogeCoin, + IconEthereum, + IconLiteCoin, + IconProps, + IconShibaInu, + IconUsdCoin, +} from '../../ui/icon'; interface InstrumentComponent { instrument: string; @@ -107,17 +131,19 @@ export function getPaymentMethodIconComponent(type?: string): ComponentType return undefined; } - const instrumentType = Object.values(instrumentTypeMap).find(record => record.instrument === type); + const instrumentType = Object.values(instrumentTypeMap).find( + (record) => record.instrument === type, + ); return instrumentType ? instrumentType.component : undefined; } function getSupportedInstrumentTypes() { - return Object.values(instrumentTypeMap).map(record => record.instrument); + return Object.values(instrumentTypeMap).map((record) => record.instrument); } export function filterInstrumentTypes(instrumentTypes: string[]) { const supportedInstrumentTypes = getSupportedInstrumentTypes(); - return instrumentTypes.filter(type => supportedInstrumentTypes.indexOf(type) !== -1); + return instrumentTypes.filter((type) => supportedInstrumentTypes.indexOf(type) !== -1); } diff --git a/packages/core/src/app/payment/creditCard/unformatCreditCardExpiryDate.spec.ts b/packages/core/src/app/payment/creditCard/unformatCreditCardExpiryDate.spec.ts index eb55e2672a..b8d741f789 100644 --- a/packages/core/src/app/payment/creditCard/unformatCreditCardExpiryDate.spec.ts +++ b/packages/core/src/app/payment/creditCard/unformatCreditCardExpiryDate.spec.ts @@ -2,28 +2,22 @@ import unformatCreditCardExpiryDate from './unformatCreditCardExpiryDate'; describe('unformatCreditCardExpiryDate()', () => { it('converts MM / YY date format into expiry date object', () => { - expect(unformatCreditCardExpiryDate('01 / 30')) - .toEqual({ month: '01', year: '2030' }); + expect(unformatCreditCardExpiryDate('01 / 30')).toEqual({ month: '01', year: '2030' }); - expect(unformatCreditCardExpiryDate('12 / 30')) - .toEqual({ month: '12', year: '2030' }); + expect(unformatCreditCardExpiryDate('12 / 30')).toEqual({ month: '12', year: '2030' }); }); it('converts MM / YYYY date format into expiry date object', () => { - expect(unformatCreditCardExpiryDate('01 / 2030')) - .toEqual({ month: '01', year: '2030' }); + expect(unformatCreditCardExpiryDate('01 / 2030')).toEqual({ month: '01', year: '2030' }); - expect(unformatCreditCardExpiryDate('12 / 2030')) - .toEqual({ month: '12', year: '2030' }); + expect(unformatCreditCardExpiryDate('12 / 2030')).toEqual({ month: '12', year: '2030' }); }); it('converts M / YY date format into expiry date object', () => { - expect(unformatCreditCardExpiryDate('1 / 30')) - .toEqual({ month: '01', year: '2030' }); + expect(unformatCreditCardExpiryDate('1 / 30')).toEqual({ month: '01', year: '2030' }); }); it('returns empty expiry date object if date format is invalid', () => { - expect(unformatCreditCardExpiryDate('fo / ba')) - .toEqual({ month: '', year: '' }); + expect(unformatCreditCardExpiryDate('fo / ba')).toEqual({ month: '', year: '' }); }); }); diff --git a/packages/core/src/app/payment/creditCard/unformatCreditCardNumber.spec.ts b/packages/core/src/app/payment/creditCard/unformatCreditCardNumber.spec.ts index 023a679f54..046721224e 100644 --- a/packages/core/src/app/payment/creditCard/unformatCreditCardNumber.spec.ts +++ b/packages/core/src/app/payment/creditCard/unformatCreditCardNumber.spec.ts @@ -2,20 +2,16 @@ import unformatCreditCardNumber from './unformatCreditCardNumber'; describe('unformatCreditCardNumber()', () => { it('removes credit card number formatting', () => { - expect(unformatCreditCardNumber('4111 1111 1111 1111')) - .toEqual('4111111111111111'); + expect(unformatCreditCardNumber('4111 1111 1111 1111')).toBe('4111111111111111'); - expect(unformatCreditCardNumber('3782 822463 10005')) - .toEqual('378282246310005'); + expect(unformatCreditCardNumber('3782 822463 10005')).toBe('378282246310005'); }); it('unformats credit card number that is partially complete', () => { - expect(unformatCreditCardNumber('4111 1111')) - .toEqual('41111111'); + expect(unformatCreditCardNumber('4111 1111')).toBe('41111111'); }); it('does not do anything if credit card number is invalid', () => { - expect(unformatCreditCardNumber('xxxx xxxx 1111 1111')) - .toEqual('xxxx xxxx 1111 1111'); + expect(unformatCreditCardNumber('xxxx xxxx 1111 1111')).toBe('xxxx xxxx 1111 1111'); }); }); diff --git a/packages/core/src/app/payment/creditCard/unformatCreditCardNumber.ts b/packages/core/src/app/payment/creditCard/unformatCreditCardNumber.ts index 9855c7f044..43534f819d 100644 --- a/packages/core/src/app/payment/creditCard/unformatCreditCardNumber.ts +++ b/packages/core/src/app/payment/creditCard/unformatCreditCardNumber.ts @@ -1,6 +1,6 @@ import { number } from 'card-validator'; -export default function unformatCreditCardNumber(value: string, separator: string = ' '): string { +export default function unformatCreditCardNumber(value: string, separator = ' '): string { const { card } = number(value); if (!card) { diff --git a/packages/core/src/app/payment/getPaymentValidationSchema.ts b/packages/core/src/app/payment/getPaymentValidationSchema.ts index da854a8dab..ead88176ee 100644 --- a/packages/core/src/app/payment/getPaymentValidationSchema.ts +++ b/packages/core/src/app/payment/getPaymentValidationSchema.ts @@ -1,6 +1,7 @@ -import { PaymentFormValues } from '@bigcommerce/checkout/payment-integration-api'; import { LanguageService } from '@bigcommerce/checkout-sdk'; -import { object, string, ObjectSchema, StringSchema } from 'yup'; +import { object, ObjectSchema, string, StringSchema } from 'yup'; + +import { PaymentFormValues } from '@bigcommerce/checkout/payment-integration-api'; import { getTermsConditionsValidationSchema } from '../termsConditions'; @@ -21,10 +22,11 @@ export default function getPaymentValidationSchema({ paymentProviderRadio: string().required(), }; - const schemaFieldsWithTerms = object(schemaFields) - .concat(getTermsConditionsValidationSchema({ isTermsConditionsRequired, language })); + const schemaFieldsWithTerms = object(schemaFields).concat( + getTermsConditionsValidationSchema({ isTermsConditionsRequired, language }), + ); - return additionalValidation ? - schemaFieldsWithTerms.concat(additionalValidation as any) : - schemaFieldsWithTerms; + return additionalValidation + ? schemaFieldsWithTerms.concat(additionalValidation as any) + : schemaFieldsWithTerms; } diff --git a/packages/core/src/app/payment/getPreselectedPayment.ts b/packages/core/src/app/payment/getPreselectedPayment.ts index 7aeed32a28..77739c343b 100644 --- a/packages/core/src/app/payment/getPreselectedPayment.ts +++ b/packages/core/src/app/payment/getPreselectedPayment.ts @@ -7,9 +7,10 @@ import { isStoreCreditPayment } from './storeCredit'; export default function getPreselectedPayment(checkout: Checkout): CheckoutPayment | undefined { const payments = checkout && checkout.payments ? checkout.payments : []; - return payments.find(payment => - !isGiftCertificatePayment(payment) - && !isStoreCreditPayment(payment) - && !!payment.providerId + return payments.find( + (payment) => + !isGiftCertificatePayment(payment) && + !isStoreCreditPayment(payment) && + !!payment.providerId, ); } diff --git a/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardCodeField.spec.tsx b/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardCodeField.spec.tsx index 1850de20be..5d8d5c96ec 100644 --- a/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardCodeField.spec.tsx +++ b/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardCodeField.spec.tsx @@ -8,7 +8,9 @@ import { createLocaleContext, LocaleContext, LocaleContextType } from '../../loc import { IconHelp } from '../../ui/icon'; import { TooltipTrigger } from '../../ui/tooltip'; -import HostedCreditCardCodeField, { HostedCreditCardCodeFieldProps } from './HostedCreditCardCodeField'; +import HostedCreditCardCodeField, { + HostedCreditCardCodeFieldProps, +} from './HostedCreditCardCodeField'; describe('HostedCreditCardCodeField', () => { let HostedCreditCardCodeFieldTest: FunctionComponent; @@ -25,24 +27,19 @@ describe('HostedCreditCardCodeField', () => { name: 'ccCvv', }; - HostedCreditCardCodeFieldTest = props => ( - - - + HostedCreditCardCodeFieldTest = (props) => ( + + + ); }); it('renders field with tooltip icon', () => { - const component = mount(); + const component = mount(); - expect(component.find(IconHelp).length) - .toEqual(1); - expect(component.find(TooltipTrigger).length) - .toEqual(1); + expect(component.find(IconHelp)).toHaveLength(1); + expect(component.find(TooltipTrigger)).toHaveLength(1); }); }); diff --git a/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardCodeField.tsx b/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardCodeField.tsx index bddc76bd30..e1e954a263 100644 --- a/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardCodeField.tsx +++ b/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardCodeField.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, Fragment, FunctionComponent } from 'react'; +import React, { FunctionComponent, useCallback, useMemo } from 'react'; import { TranslatedString } from '../../locale'; import { FormField, TextInputIframeContainer } from '../../ui/form'; @@ -17,37 +17,42 @@ const HostedCreditCardCodeField: FunctionComponent { - const renderInput = useCallback(() => (<> - - - - ), [id, appearFocused]); - - const labelContent = useMemo(() => ( - - + const renderInput = useCallback( + () => ( + <> + + + + + ), + [id, appearFocused], + ); - } - > - - - - - - ), []); + const labelContent = useMemo( + () => ( + <> + + + }> + + + + + + ), + [], + ); return ( ); }; diff --git a/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardExpiryField.tsx b/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardExpiryField.tsx index 454f40886d..2b61d32ec7 100644 --- a/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardExpiryField.tsx +++ b/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardExpiryField.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, FunctionComponent } from 'react'; +import React, { FunctionComponent, useCallback } from 'react'; import { TranslatedString } from '../../locale'; import { FormField, TextInputIframeContainer } from '../../ui/form'; @@ -14,19 +14,17 @@ const HostedCreditCardExpiryField: FunctionComponent { - const renderInput = useCallback(() => (<> - - ), [id, appearFocused]); + const renderInput = useCallback( + () => , + [id, appearFocused], + ); return ( } - name={ name } + input={renderInput} + labelContent={} + name={name} /> ); }; diff --git a/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardFieldset.spec.tsx b/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardFieldset.spec.tsx index 4bb6e8587b..cdb60184dc 100644 --- a/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardFieldset.spec.tsx +++ b/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardFieldset.spec.tsx @@ -5,11 +5,12 @@ import React, { FunctionComponent } from 'react'; import HostedCreditCardCodeField from './HostedCreditCardCodeField'; import HostedCreditCardExpiryField from './HostedCreditCardExpiryField'; -import HostedCreditCardFieldset, { HostedCreditCardFieldsetProps } from './HostedCreditCardFieldset'; +import HostedCreditCardFieldset, { + HostedCreditCardFieldsetProps, +} from './HostedCreditCardFieldset'; import HostedCreditCardNameField from './HostedCreditCardNameField'; import HostedCreditCardNumberField from './HostedCreditCardNumberField'; -/* eslint-disable react/jsx-no-bind */ describe('HostedCreditCardFieldset', () => { let defaultProps: HostedCreditCardFieldsetProps; let HostedCreditCardFieldsetTest: FunctionComponent; @@ -20,65 +21,56 @@ describe('HostedCreditCardFieldset', () => { cardNumberId: 'cardNumber', }; - HostedCreditCardFieldsetTest = props => ( + HostedCreditCardFieldsetTest = (props) => ( } + initialValues={{ hostedForm: {} }} + onSubmit={noop} + render={() => } /> ); }); it('renders required field containers', () => { - const component = mount( - - ); + const component = mount(); - expect(component.find(HostedCreditCardNumberField).length) - .toEqual(1); + expect(component.find(HostedCreditCardNumberField)).toHaveLength(1); - expect(component.find(HostedCreditCardExpiryField).length) - .toEqual(1); + expect(component.find(HostedCreditCardExpiryField)).toHaveLength(1); }); it('renders optional field containers', () => { const component = mount( + />, ); - expect(component.find(HostedCreditCardCodeField).length) - .toEqual(1); + expect(component.find(HostedCreditCardCodeField)).toHaveLength(1); - expect(component.find(HostedCreditCardNameField).length) - .toEqual(1); + expect(component.find(HostedCreditCardNameField)).toHaveLength(1); }); it('renders additional fields if provided', () => { const component = mount( } - /> + {...defaultProps} + additionalFields={} + />, ); - expect(component.find('input[name="foobar"]').length) - .toEqual(1); + expect(component.find('input[name="foobar"]')).toHaveLength(1); }); it('renders field container with focus styles', () => { const component = mount( - + , ); - expect(component.find('TextInputIframeContainer[id="cardNumber"]').prop('appearFocused')) - .toBeTruthy(); + expect( + component.find('TextInputIframeContainer[id="cardNumber"]').prop('appearFocused'), + ).toBeTruthy(); }); }); /* eslint-enable react/jsx-no-bind */ diff --git a/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardFieldset.tsx b/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardFieldset.tsx index 230f6de432..ed61366c98 100644 --- a/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardFieldset.tsx +++ b/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardFieldset.tsx @@ -34,30 +34,34 @@ const HostedCreditCardFieldset: FunctionComponent >
    - { cardNameId && } + {cardNameId && ( + + )} - { cardCodeId && } + {cardCodeId && ( + + )} - { additionalFields } + {additionalFields}
    ); diff --git a/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardNameField.tsx b/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardNameField.tsx index f11a4d11f0..21fef92b89 100644 --- a/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardNameField.tsx +++ b/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardNameField.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, FunctionComponent } from 'react'; +import React, { FunctionComponent, useCallback } from 'react'; import { TranslatedString } from '../../locale'; import { FormField, TextInputIframeContainer } from '../../ui/form'; @@ -14,19 +14,17 @@ const HostedCreditCardNameField: FunctionComponent { - const renderInput = useCallback(() => (<> - - ), [id, appearFocused]); + const renderInput = useCallback( + () => , + [id, appearFocused], + ); return ( } - name={ name } + input={renderInput} + labelContent={} + name={name} /> ); }; diff --git a/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardNumberField.spec.tsx b/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardNumberField.spec.tsx index d1c75e3660..5ae89ba538 100644 --- a/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardNumberField.spec.tsx +++ b/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardNumberField.spec.tsx @@ -7,7 +7,9 @@ import { getStoreConfig } from '../../config/config.mock'; import { createLocaleContext, LocaleContext, LocaleContextType } from '../../locale'; import { FormField } from '../../ui/form'; -import HostedCreditCardNumberField, { HostedCreditCardNumberFieldProps } from './HostedCreditCardNumberField'; +import HostedCreditCardNumberField, { + HostedCreditCardNumberFieldProps, +} from './HostedCreditCardNumberField'; describe('HostedCreditCardNumberField', () => { let HostedCreditCardNumberFieldTest: FunctionComponent; @@ -24,22 +26,20 @@ describe('HostedCreditCardNumberField', () => { name: 'ccNumber', }; - HostedCreditCardNumberFieldTest = props => ( - - - + HostedCreditCardNumberFieldTest = (props) => ( + + + ); }); it('renders field with expected class name', () => { - const component = mount(); + const component = mount(); - expect(component.find(FormField).prop('additionalClassName')) - .toContain('form-field--ccNumber'); + expect(component.find(FormField).prop('additionalClassName')).toContain( + 'form-field--ccNumber', + ); }); }); diff --git a/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardNumberField.tsx b/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardNumberField.tsx index 2f04cf6731..cdbc51130f 100644 --- a/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardNumberField.tsx +++ b/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardNumberField.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, FunctionComponent } from 'react'; +import React, { FunctionComponent, useCallback } from 'react'; import { TranslatedString } from '../../locale'; import { FormField, TextInputIframeContainer } from '../../ui/form'; @@ -15,22 +15,27 @@ const HostedCreditCardNumberField: FunctionComponent { - const renderInput = useCallback(() => (<> - + const renderInput = useCallback( + () => ( + <> + - - ), [id, appearFocused]); + + + ), + [id, appearFocused], + ); return ( } - name={ name } + input={renderInput} + labelContent={} + name={name} /> ); }; diff --git a/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardValidation.spec.tsx b/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardValidation.spec.tsx index 0938ca1761..91569cc137 100644 --- a/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardValidation.spec.tsx +++ b/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardValidation.spec.tsx @@ -8,7 +8,9 @@ import { createLocaleContext, LocaleContext } from '../../locale'; import HostedCreditCardCodeField from './HostedCreditCardCodeField'; import HostedCreditCardNumberField from './HostedCreditCardNumberField'; -import HostedCreditCardValidation, { HostedCreditCardValidationProps } from './HostedCreditCardValidation'; +import HostedCreditCardValidation, { + HostedCreditCardValidationProps, +} from './HostedCreditCardValidation'; describe('HostedCreditCardValidation', () => { let HostedCreditCardValidationTest: FunctionComponent; @@ -16,48 +18,36 @@ describe('HostedCreditCardValidation', () => { beforeEach(() => { const localeContext = createLocaleContext(getStoreConfig()); - HostedCreditCardValidationTest = props => ( - - - + HostedCreditCardValidationTest = (props) => ( + + + ); }); it('shows card number field if configured', () => { - const component = mount( - - ); + const component = mount(); - expect(component.find(HostedCreditCardNumberField).length) - .toEqual(1); + expect(component.find(HostedCreditCardNumberField)).toHaveLength(1); }); it('hides card number field if configured', () => { - const component = mount( - - ); + const component = mount(); - expect(component.find(HostedCreditCardNumberField).length) - .toEqual(0); + expect(component.find(HostedCreditCardNumberField)).toHaveLength(0); }); it('shows card code field if configured', () => { - const component = mount( - - ); + const component = mount(); - expect(component.find(HostedCreditCardCodeField).length) - .toEqual(1); + expect(component.find(HostedCreditCardCodeField)).toHaveLength(1); }); it('hides card code field if configured', () => { - const component = mount( - - ); + const component = mount(); - expect(component.find(HostedCreditCardCodeField).length) - .toEqual(0); + expect(component.find(HostedCreditCardCodeField)).toHaveLength(0); }); }); diff --git a/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardValidation.tsx b/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardValidation.tsx index 8386d68626..dff0e83737 100644 --- a/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardValidation.tsx +++ b/packages/core/src/app/payment/hostedCreditCard/HostedCreditCardValidation.tsx @@ -15,30 +15,38 @@ const HostedCreditCardValidation: FunctionComponent (<> - { cardNumberId &&

    - - - - -
    - - -

    } - -
    - { cardNumberId && } - - { cardCodeId && } -
    -); +}) => ( + <> + {cardNumberId && ( +

    + + + + +
    + + +

    + )} + +
    + {cardNumberId && ( + + )} + + {cardCodeId && ( + + )} +
    + +); export default HostedCreditCardValidation; diff --git a/packages/core/src/app/payment/hostedCreditCard/getHostedCreditCardValidationSchema.spec.ts b/packages/core/src/app/payment/hostedCreditCard/getHostedCreditCardValidationSchema.spec.ts index cc1afc496d..a37b5e4a6a 100644 --- a/packages/core/src/app/payment/hostedCreditCard/getHostedCreditCardValidationSchema.spec.ts +++ b/packages/core/src/app/payment/hostedCreditCard/getHostedCreditCardValidationSchema.spec.ts @@ -1,7 +1,8 @@ -import { HostedCreditCardFieldsetValues } from '@bigcommerce/checkout/payment-integration-api'; import { LanguageService } from '@bigcommerce/checkout-sdk'; import { ObjectSchema } from 'yup'; +import { HostedCreditCardFieldsetValues } from '@bigcommerce/checkout/payment-integration-api'; + import getHostedCreditCardValidationSchema from './getHostedCreditCardValidationSchema'; describe('getHostedCreditCardValidationSchema', () => { @@ -11,7 +12,7 @@ describe('getHostedCreditCardValidationSchema', () => { beforeEach(() => { language = { - translate: jest.fn(key => key), + translate: jest.fn((key) => key), }; values = { hostedForm: {} }; @@ -22,56 +23,58 @@ describe('getHostedCreditCardValidationSchema', () => { }); it('does not throw error if data is valid', () => { - expect(() => schema.validateSync(values)) - .not.toThrow(); + expect(() => schema.validateSync(values)).not.toThrow(); }); it('throws error if card code field is missing', () => { values.hostedForm.errors = { cardCode: 'required' }; - expect(() => schema.validateSync(values)) - .toThrowError('payment.credit_card_cvv_required_error'); + expect(() => schema.validateSync(values)).toThrow('payment.credit_card_cvv_required_error'); }); it('throws error if card code field is invalid', () => { values.hostedForm.errors = { cardCode: 'invalid_card_code' }; - expect(() => schema.validateSync(values)) - .toThrowError('payment.credit_card_cvv_invalid_error'); + expect(() => schema.validateSync(values)).toThrow('payment.credit_card_cvv_invalid_error'); }); it('throws error if card expiry field is missing', () => { values.hostedForm.errors = { cardExpiry: 'required' }; - expect(() => schema.validateSync(values)) - .toThrowError('payment.credit_card_expiration_required_error'); + expect(() => schema.validateSync(values)).toThrow( + 'payment.credit_card_expiration_required_error', + ); }); it('throws error if card expiry field is invalid', () => { values.hostedForm.errors = { cardExpiry: 'invalid_card_expiry' }; - expect(() => schema.validateSync(values)) - .toThrowError('payment.credit_card_expiration_invalid_error'); + expect(() => schema.validateSync(values)).toThrow( + 'payment.credit_card_expiration_invalid_error', + ); }); it('throws error if card name field is missing', () => { values.hostedForm.errors = { cardName: 'required' }; - expect(() => schema.validateSync(values)) - .toThrowError('payment.credit_card_name_required_error'); + expect(() => schema.validateSync(values)).toThrow( + 'payment.credit_card_name_required_error', + ); }); it('throws error if card number field is missing', () => { values.hostedForm.errors = { cardNumber: 'required' }; - expect(() => schema.validateSync(values)) - .toThrowError('payment.credit_card_number_required_error'); + expect(() => schema.validateSync(values)).toThrow( + 'payment.credit_card_number_required_error', + ); }); it('throws error if card number field is invalid', () => { values.hostedForm.errors = { cardNumber: 'invalid_card_number' }; - expect(() => schema.validateSync(values)) - .toThrowError('payment.credit_card_number_invalid_error'); + expect(() => schema.validateSync(values)).toThrow( + 'payment.credit_card_number_invalid_error', + ); }); }); diff --git a/packages/core/src/app/payment/hostedCreditCard/getHostedCreditCardValidationSchema.ts b/packages/core/src/app/payment/hostedCreditCard/getHostedCreditCardValidationSchema.ts index f22030cd39..718b9a5414 100644 --- a/packages/core/src/app/payment/hostedCreditCard/getHostedCreditCardValidationSchema.ts +++ b/packages/core/src/app/payment/hostedCreditCard/getHostedCreditCardValidationSchema.ts @@ -1,6 +1,6 @@ import { LanguageService } from '@bigcommerce/checkout-sdk'; import { memoize } from '@bigcommerce/memoize'; -import { object, string, ObjectSchema } from 'yup'; +import { object, ObjectSchema, string } from 'yup'; export interface HostedCreditCardValidationSchemaOptions { language: LanguageService; @@ -26,37 +26,38 @@ export default memoize(function getHostedCreditCardValidationSchema({ cardCode: string() .test({ message: language.translate('payment.credit_card_cvv_required_error'), - test: value => value !== 'required', + test: (value) => value !== 'required', }) .test({ message: language.translate('payment.credit_card_cvv_invalid_error'), - test: value => value !== 'invalid_card_code', + test: (value) => value !== 'invalid_card_code', }), cardExpiry: string() .test({ - message: language.translate('payment.credit_card_expiration_required_error'), - test: value => value !== 'required', + message: language.translate( + 'payment.credit_card_expiration_required_error', + ), + test: (value) => value !== 'required', }) .test({ message: language.translate('payment.credit_card_expiration_invalid_error'), - test: value => value !== 'invalid_card_expiry', + test: (value) => value !== 'invalid_card_expiry', }), - cardName: string() - .test({ - message: language.translate('payment.credit_card_name_required_error'), - test: value => value !== 'required', - }), + cardName: string().test({ + message: language.translate('payment.credit_card_name_required_error'), + test: (value) => value !== 'required', + }), cardNumber: string() .test({ message: language.translate('payment.credit_card_number_required_error'), - test: value => value !== 'required', + test: (value) => value !== 'required', }) .test({ message: language.translate('payment.credit_card_number_invalid_error'), - test: value => value !== 'invalid_card_number', + test: (value) => value !== 'invalid_card_number', }), }), }), diff --git a/packages/core/src/app/payment/hostedCreditCard/getHostedInstrumentValidationSchema.spec.ts b/packages/core/src/app/payment/hostedCreditCard/getHostedInstrumentValidationSchema.spec.ts index 2eed15fa68..60e58f2a75 100644 --- a/packages/core/src/app/payment/hostedCreditCard/getHostedInstrumentValidationSchema.spec.ts +++ b/packages/core/src/app/payment/hostedCreditCard/getHostedInstrumentValidationSchema.spec.ts @@ -1,7 +1,8 @@ -import { HostedCreditCardValidationValues } from '@bigcommerce/checkout/payment-integration-api'; import { LanguageService } from '@bigcommerce/checkout-sdk'; import { ObjectSchema } from 'yup'; +import { HostedCreditCardValidationValues } from '@bigcommerce/checkout/payment-integration-api'; + import getHostedInstrumentValidationSchema from './getHostedInstrumentValidationSchema'; describe('getHostedInstrumentValidationSchema', () => { @@ -11,7 +12,7 @@ describe('getHostedInstrumentValidationSchema', () => { beforeEach(() => { language = { - translate: jest.fn(key => key), + translate: jest.fn((key) => key), }; values = { @@ -25,49 +26,48 @@ describe('getHostedInstrumentValidationSchema', () => { }); it('does not throw error if data is valid', () => { - expect(() => schema.validateSync(values)) - .not.toThrow(); + expect(() => schema.validateSync(values)).not.toThrow(); }); it('throws error if instrument ID is missing', () => { values.instrumentId = ''; - expect(() => schema.validateSync(values)) - .toThrowError('required'); + expect(() => schema.validateSync(values)).toThrow('required'); }); it('throws error if card code field is missing', () => { values.hostedForm.errors = { cardCodeVerification: 'required' }; - expect(() => schema.validateSync(values)) - .toThrowError('payment.credit_card_cvv_required_error'); + expect(() => schema.validateSync(values)).toThrow('payment.credit_card_cvv_required_error'); }); it('throws error if card code field is invalid', () => { values.hostedForm.errors = { cardCodeVerification: 'invalid_card_code' }; - expect(() => schema.validateSync(values)) - .toThrowError('payment.credit_card_cvv_invalid_error'); + expect(() => schema.validateSync(values)).toThrow('payment.credit_card_cvv_invalid_error'); }); it('throws error if card number field is missing', () => { values.hostedForm.errors = { cardNumberVerification: 'required' }; - expect(() => schema.validateSync(values)) - .toThrowError('payment.credit_card_number_required_error'); + expect(() => schema.validateSync(values)).toThrow( + 'payment.credit_card_number_required_error', + ); }); it('throws error if card number field is invalid', () => { values.hostedForm.errors = { cardNumberVerification: 'invalid_card_number' }; - expect(() => schema.validateSync(values)) - .toThrowError('payment.credit_card_number_invalid_error'); + expect(() => schema.validateSync(values)).toThrow( + 'payment.credit_card_number_invalid_error', + ); }); it('throws error if card number field does not match with stored instrument', () => { values.hostedForm.errors = { cardNumberVerification: 'mismatched_card_number' }; - expect(() => schema.validateSync(values)) - .toThrowError('payment.credit_card_number_mismatch_error'); + expect(() => schema.validateSync(values)).toThrow( + 'payment.credit_card_number_mismatch_error', + ); }); }); diff --git a/packages/core/src/app/payment/hostedCreditCard/getHostedInstrumentValidationSchema.ts b/packages/core/src/app/payment/hostedCreditCard/getHostedInstrumentValidationSchema.ts index 2b0f004e9b..c711c36b98 100644 --- a/packages/core/src/app/payment/hostedCreditCard/getHostedInstrumentValidationSchema.ts +++ b/packages/core/src/app/payment/hostedCreditCard/getHostedInstrumentValidationSchema.ts @@ -1,6 +1,6 @@ import { LanguageService } from '@bigcommerce/checkout-sdk'; import { memoize } from '@bigcommerce/memoize'; -import { object, string, ObjectSchema } from 'yup'; +import { object, ObjectSchema, string } from 'yup'; export interface HostedInstrumentValidationSchemaOptions { language: LanguageService; @@ -20,33 +20,32 @@ export default memoize(function getHostedInstrumentValidationSchema({ language, }: HostedInstrumentValidationSchemaOptions): ObjectSchema { return object({ - instrumentId: string() - .required(), + instrumentId: string().required(), hostedForm: object({ errors: object({ cardCodeVerification: string() .test({ message: language.translate('payment.credit_card_cvv_required_error'), - test: value => value !== 'required', + test: (value) => value !== 'required', }) .test({ message: language.translate('payment.credit_card_cvv_invalid_error'), - test: value => value !== 'invalid_card_code', + test: (value) => value !== 'invalid_card_code', }), cardNumberVerification: string() .test({ message: language.translate('payment.credit_card_number_required_error'), - test: value => value !== 'required', + test: (value) => value !== 'required', }) .test({ message: language.translate('payment.credit_card_number_invalid_error'), - test: value => value !== 'invalid_card_number', + test: (value) => value !== 'invalid_card_number', }) .test({ message: language.translate('payment.credit_card_number_mismatch_error'), - test: value => value !== 'mismatched_card_number', + test: (value) => value !== 'mismatched_card_number', }), }), }), diff --git a/packages/core/src/app/payment/hostedCreditCard/index.ts b/packages/core/src/app/payment/hostedCreditCard/index.ts index fe115cf45d..bd88d4eeee 100644 --- a/packages/core/src/app/payment/hostedCreditCard/index.ts +++ b/packages/core/src/app/payment/hostedCreditCard/index.ts @@ -1,2 +1,5 @@ -export { default as withHostedCreditCardFieldset, WithInjectedHostedCreditCardFieldsetProps } from './withHostedCreditCardFieldset'; +export { + default as withHostedCreditCardFieldset, + WithInjectedHostedCreditCardFieldsetProps, +} from './withHostedCreditCardFieldset'; export { default as HostedCreditCardFieldset } from './HostedCreditCardFieldset'; diff --git a/packages/core/src/app/payment/hostedCreditCard/withHostedCreditCardFieldset.spec.tsx b/packages/core/src/app/payment/hostedCreditCard/withHostedCreditCardFieldset.spec.tsx index 4b3bda0c95..e60b8a43c0 100644 --- a/packages/core/src/app/payment/hostedCreditCard/withHostedCreditCardFieldset.spec.tsx +++ b/packages/core/src/app/payment/hostedCreditCard/withHostedCreditCardFieldset.spec.tsx @@ -1,40 +1,53 @@ -import { CardInstrumentFieldsetValues, HostedCreditCardFieldsetValues } from '@bigcommerce/checkout/payment-integration-api'; -import { createCheckoutService, CheckoutSelectors, CheckoutService, HostedFieldType } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, + HostedFieldType, +} from '@bigcommerce/checkout-sdk'; import { mount } from 'enzyme'; import { Formik, FormikProps } from 'formik'; import { last, merge, noop } from 'lodash'; import React, { ComponentType, FunctionComponent, ReactNode } from 'react'; import { act } from 'react-dom/test-utils'; +import { + CardInstrumentFieldsetValues, + HostedCreditCardFieldsetValues, +} from '@bigcommerce/checkout/payment-integration-api'; + import { getCart } from '../../cart/carts.mock'; import { CheckoutProvider } from '../../checkout'; import { getStoreConfig } from '../../config/config.mock'; import { getCustomer } from '../../customer/customers.mock'; import { createLocaleContext, LocaleContext, LocaleContextType } from '../../locale'; import { FormContext, FormContextType } from '../../ui/form'; -import { getCreditCardInputStyles, CreditCardInputStylesType } from '../creditCard'; +import { CreditCardInputStylesType, getCreditCardInputStyles } from '../creditCard'; import { getPaymentMethod } from '../payment-methods.mock'; -import { getCardInstrument } from '../storedInstrument/instruments.mock'; import PaymentContext, { PaymentContextProps } from '../PaymentContext'; +import { getCardInstrument } from '../storedInstrument/instruments.mock'; -import withHostedCreditCardFieldset, { WithHostedCreditCardFieldsetProps, WithInjectedHostedCreditCardFieldsetProps } from './withHostedCreditCardFieldset'; import HostedCreditCardFieldset from './HostedCreditCardFieldset'; +import withHostedCreditCardFieldset, { + WithHostedCreditCardFieldsetProps, + WithInjectedHostedCreditCardFieldsetProps, +} from './withHostedCreditCardFieldset'; jest.mock('../creditCard', () => ({ ...jest.requireActual('../creditCard'), - getCreditCardInputStyles: jest.fn, Parameters>( - (_containerId, _fieldType, type) => { - if (type === CreditCardInputStylesType.Error) { - return Promise.resolve({ color: 'rgb(255, 0, 0)' }); - } - - if (type === CreditCardInputStylesType.Focus) { - return Promise.resolve({ color: 'rgb(0, 0, 255)' }); - } + getCreditCardInputStyles: jest.fn< + ReturnType, + Parameters + >((_containerId, _fieldType, type) => { + if (type === CreditCardInputStylesType.Error) { + return Promise.resolve({ color: 'rgb(255, 0, 0)' }); + } - return Promise.resolve({ color: 'rgb(0, 0, 0)' }); + if (type === CreditCardInputStylesType.Focus) { + return Promise.resolve({ color: 'rgb(0, 0, 255)' }); } - ), + + return Promise.resolve({ color: 'rgb(0, 0, 0)' }); + }), })); describe('withHostedCreditCardFieldset', () => { @@ -42,14 +55,19 @@ describe('withHostedCreditCardFieldset', () => { let checkoutState: CheckoutSelectors; let defaultProps: WithHostedCreditCardFieldsetProps; let formContext: FormContextType; - let formikRender: jest.Mock]>; + let formikRender: jest.Mock< + ReactNode, + [FormikProps] + >; let formikProps: FormikProps; let initialValues: HostedCreditCardFieldsetValues & CardInstrumentFieldsetValues; let localeContext: LocaleContextType; let paymentContext: PaymentContextProps; let DecoratedPaymentMethod: ComponentType; let DecoratedPaymentMethodTest: FunctionComponent; - let InnerPaymentMethod: ComponentType; + let InnerPaymentMethod: ComponentType< + WithHostedCreditCardFieldsetProps & WithInjectedHostedCreditCardFieldsetProps + >; beforeEach(() => { checkoutService = createCheckoutService(); @@ -81,46 +99,43 @@ describe('withHostedCreditCardFieldset', () => { setSubmitted: jest.fn(), }; - jest.spyOn(checkoutState.data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(checkoutState.data, 'getCart').mockReturnValue(getCart()); - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - jest.spyOn(checkoutState.data, 'getCustomer') - .mockReturnValue(getCustomer()); + jest.spyOn(checkoutState.data, 'getCustomer').mockReturnValue(getCustomer()); - jest.spyOn(checkoutState.data, 'getPaymentMethod') - .mockReturnValue(defaultProps.method); + jest.spyOn(checkoutState.data, 'getPaymentMethod').mockReturnValue(defaultProps.method); - InnerPaymentMethod = jest.fn(({ - getHostedStoredCardValidationFieldset = noop, - hostedFieldset, - }) => { - return <> - { hostedFieldset } - { getHostedStoredCardValidationFieldset() } - ; - }); + InnerPaymentMethod = jest.fn( + ({ getHostedStoredCardValidationFieldset = noop, hostedFieldset }) => { + return ( + <> + {hostedFieldset} + {getHostedStoredCardValidationFieldset()} + + ); + }, + ); DecoratedPaymentMethod = withHostedCreditCardFieldset(InnerPaymentMethod); - DecoratedPaymentMethodTest = props => { - formikRender = jest.fn(renderProps => { + DecoratedPaymentMethodTest = (props) => { + formikRender = jest.fn((renderProps) => { formikProps = renderProps; - return ; + return ; }); return ( - - - - + + + + @@ -131,199 +146,247 @@ describe('withHostedCreditCardFieldset', () => { }); it('renders hosted credit card fieldset', () => { - const container = mount(); + const container = mount(); - expect(container.find(HostedCreditCardFieldset).length) - .toEqual(1); + expect(container.find(HostedCreditCardFieldset)).toHaveLength(1); }); it('does not render hosted credit card fieldset if feature is not enabled', () => { - const container = mount(); + const container = mount( + , + ); - expect(container.find(HostedCreditCardFieldset).length) - .toEqual(0); + expect(container.find(HostedCreditCardFieldset)).toHaveLength(0); }); it('passes hosted form configuration to inner component', async () => { - const container = mount(); - const getHostedFormOptions = container.find(InnerPaymentMethod).prop('getHostedFormOptions'); - - expect(await getHostedFormOptions()) - .toEqual({ - fields: { - cardCode: { accessibilityLabel: 'CVV', containerId: 'authorizenet-ccCvv' }, - cardExpiry: { accessibilityLabel: 'Expiration', containerId: 'authorizenet-ccExpiry', placeholder: 'MM / YY' }, - cardName: { accessibilityLabel: 'Name on Card', containerId: 'authorizenet-ccName' }, - cardNumber: { accessibilityLabel: 'Credit Card Number', containerId: 'authorizenet-ccNumber' }, + const container = mount(); + const getHostedFormOptions = container + .find(InnerPaymentMethod) + .prop('getHostedFormOptions'); + + expect(await getHostedFormOptions()).toEqual({ + fields: { + cardCode: { accessibilityLabel: 'CVV', containerId: 'authorizenet-ccCvv' }, + cardExpiry: { + accessibilityLabel: 'Expiration', + containerId: 'authorizenet-ccExpiry', + placeholder: 'MM / YY', }, - styles: { - default: expect.any(Object), - error: expect.any(Object), - focus: expect.any(Object), + cardName: { + accessibilityLabel: 'Name on Card', + containerId: 'authorizenet-ccName', }, - onBlur: expect.any(Function), - onCardTypeChange: expect.any(Function), - onEnter: expect.any(Function), - onFocus: expect.any(Function), - onValidate: expect.any(Function), - }); + cardNumber: { + accessibilityLabel: 'Credit Card Number', + containerId: 'authorizenet-ccNumber', + }, + }, + styles: { + default: expect.any(Object), + error: expect.any(Object), + focus: expect.any(Object), + }, + onBlur: expect.any(Function), + onCardTypeChange: expect.any(Function), + onEnter: expect.any(Function), + onFocus: expect.any(Function), + onValidate: expect.any(Function), + }); }); it('passes hosted verification form options to inner component when there is selected instrument', async () => { - const container = mount(); - const getHostedFormOptions = container.find(InnerPaymentMethod).prop('getHostedFormOptions'); - - expect(await getHostedFormOptions({ - ...getCardInstrument(), - trustedShippingAddress: false, - })) - .toEqual({ - fields: { - cardCodeVerification: { - accessibilityLabel: 'CVV', - containerId: 'authorizenet-ccCvv', - instrumentId: getCardInstrument().bigpayToken, - }, - cardNumberVerification: { - accessibilityLabel: 'Credit Card Number', - containerId: 'authorizenet-ccNumber', - instrumentId: getCardInstrument().bigpayToken, - }, + const container = mount(); + const getHostedFormOptions = container + .find(InnerPaymentMethod) + .prop('getHostedFormOptions'); + + expect( + await getHostedFormOptions({ + ...getCardInstrument(), + trustedShippingAddress: false, + }), + ).toEqual({ + fields: { + cardCodeVerification: { + accessibilityLabel: 'CVV', + containerId: 'authorizenet-ccCvv', + instrumentId: getCardInstrument().bigpayToken, }, - styles: { - default: expect.any(Object), - error: expect.any(Object), - focus: expect.any(Object), + cardNumberVerification: { + accessibilityLabel: 'Credit Card Number', + containerId: 'authorizenet-ccNumber', + instrumentId: getCardInstrument().bigpayToken, }, - onBlur: expect.any(Function), - onCardTypeChange: expect.any(Function), - onEnter: expect.any(Function), - onFocus: expect.any(Function), - onValidate: expect.any(Function), - }); + }, + styles: { + default: expect.any(Object), + error: expect.any(Object), + focus: expect.any(Object), + }, + onBlur: expect.any(Function), + onCardTypeChange: expect.any(Function), + onEnter: expect.any(Function), + onFocus: expect.any(Function), + onValidate: expect.any(Function), + }); }); it('passes styling properties to hosted form', async () => { - const container = mount(); - const getHostedFormOptions = container.find(InnerPaymentMethod).prop('getHostedFormOptions'); + const container = mount(); + const getHostedFormOptions = container + .find(InnerPaymentMethod) + .prop('getHostedFormOptions'); - expect(await getHostedFormOptions()) - .toEqual(expect.objectContaining({ + expect(await getHostedFormOptions()).toEqual( + expect.objectContaining({ styles: { default: { color: 'rgb(0, 0, 0)' }, error: { color: 'rgb(255, 0, 0)' }, focus: { color: 'rgb(0, 0, 255)' }, }, - })); - - expect(getCreditCardInputStyles) - .toHaveBeenCalledWith('authorizenet-ccNumber', ['color', 'fontFamily', 'fontSize', 'fontWeight']); - expect(getCreditCardInputStyles) - .toHaveBeenCalledWith('authorizenet-ccNumber', ['color', 'fontFamily', 'fontSize', 'fontWeight'], CreditCardInputStylesType.Error); - expect(getCreditCardInputStyles) - .toHaveBeenCalledWith('authorizenet-ccNumber', ['color', 'fontFamily', 'fontSize', 'fontWeight'], CreditCardInputStylesType.Focus); + }), + ); + + expect(getCreditCardInputStyles).toHaveBeenCalledWith('authorizenet-ccNumber', [ + 'color', + 'fontFamily', + 'fontSize', + 'fontWeight', + ]); + expect(getCreditCardInputStyles).toHaveBeenCalledWith( + 'authorizenet-ccNumber', + ['color', 'fontFamily', 'fontSize', 'fontWeight'], + CreditCardInputStylesType.Error, + ); + expect(getCreditCardInputStyles).toHaveBeenCalledWith( + 'authorizenet-ccNumber', + ['color', 'fontFamily', 'fontSize', 'fontWeight'], + CreditCardInputStylesType.Focus, + ); }); it('passes error messages from hosted form to Formik form', async () => { - const container = mount(); - const getHostedFormOptions = container.find(InnerPaymentMethod).prop('getHostedFormOptions'); + const container = mount(); + const getHostedFormOptions = container + .find(InnerPaymentMethod) + .prop('getHostedFormOptions'); const { onValidate } = await getHostedFormOptions(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - onValidate!({ + + onValidate({ isValid: false, errors: { cardCode: [], cardExpiry: [], cardName: [], - cardNumber: [{ fieldType: 'cardNumber', type: 'required', message: 'Card number is required' }], + cardNumber: [ + { + fieldType: 'cardNumber', + type: 'required', + message: 'Card number is required', + }, + ], }, }); container.update(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect((last(formikRender.mock.calls)![0].values as HostedCreditCardFieldsetValues).hostedForm.errors) - .toEqual(expect.objectContaining({ + + expect( + (last(formikRender.mock.calls)![0].values as HostedCreditCardFieldsetValues).hostedForm + .errors, + ).toEqual( + expect.objectContaining({ cardNumber: 'required', - })); + }), + ); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(last(formikRender.mock.calls)![0].touched) - .toEqual(expect.objectContaining({ + expect(last(formikRender.mock.calls)![0].touched).toEqual( + expect.objectContaining({ hostedForm: { errors: { cardNumber: true, }, }, - })); + }), + ); }); it('passes card type from hosted form to Formik form', async () => { - const container = mount(); - const getHostedFormOptions = container.find(InnerPaymentMethod).prop('getHostedFormOptions'); + const container = mount(); + const getHostedFormOptions = container + .find(InnerPaymentMethod) + .prop('getHostedFormOptions'); const { onCardTypeChange } = await getHostedFormOptions(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - onCardTypeChange!({ cardType: 'mastercard' }); + + onCardTypeChange({ cardType: 'mastercard' }); container.update(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect((last(formikRender.mock.calls)![0].values as HostedCreditCardFieldsetValues).hostedForm.cardType) - .toEqual('mastercard'); + + expect( + (last(formikRender.mock.calls)![0].values as HostedCreditCardFieldsetValues).hostedForm + .cardType, + ).toBe('mastercard'); }); it('highlights hosted field in focus', async () => { - const container = mount(); - const getHostedFormOptions = container.find(InnerPaymentMethod).prop('getHostedFormOptions'); + const container = mount(); + const getHostedFormOptions = container + .find(InnerPaymentMethod) + .prop('getHostedFormOptions'); const { onFocus } = await getHostedFormOptions(); act(() => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - onFocus!({ fieldType: 'cardNumber' as HostedFieldType }); + + onFocus({ fieldType: 'cardNumber' as HostedFieldType }); }); container.update(); - expect(container.find(HostedCreditCardFieldset).prop('focusedFieldType')) - .toEqual('cardNumber'); + expect(container.find(HostedCreditCardFieldset).prop('focusedFieldType')).toBe( + 'cardNumber', + ); }); it('clears highlight if hosted field in no longer in focus', async () => { - const container = mount(); - const getHostedFormOptions = container.find(InnerPaymentMethod).prop('getHostedFormOptions'); + const container = mount(); + const getHostedFormOptions = container + .find(InnerPaymentMethod) + .prop('getHostedFormOptions'); const { onBlur } = await getHostedFormOptions(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - onBlur!({ fieldType: 'cardNumber' as HostedFieldType }); + + onBlur({ fieldType: 'cardNumber' as HostedFieldType }); container.update(); - expect(container.find(HostedCreditCardFieldset).prop('focusedFieldType')) - .toEqual(undefined); + expect(container.find(HostedCreditCardFieldset).prop('focusedFieldType')).toBeUndefined(); }); it('submits form when enter key is pressed', async () => { - const container = mount(); - const getHostedFormOptions = container.find(InnerPaymentMethod).prop('getHostedFormOptions'); + const container = mount(); + const getHostedFormOptions = container + .find(InnerPaymentMethod) + .prop('getHostedFormOptions'); const { onEnter } = await getHostedFormOptions(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - onEnter!({ fieldType: 'cardNumber' as HostedFieldType }); + + onEnter({ fieldType: 'cardNumber' as HostedFieldType }); container.update(); - expect(formikProps.submitCount) - .toEqual(1); - expect(formContext.setSubmitted) - .toHaveBeenCalled(); + expect(formikProps.submitCount).toBe(1); + expect(formContext.setSubmitted).toHaveBeenCalled(); }); }); diff --git a/packages/core/src/app/payment/hostedCreditCard/withHostedCreditCardFieldset.tsx b/packages/core/src/app/payment/hostedCreditCard/withHostedCreditCardFieldset.tsx index 2ef31c34f9..a3da93a94b 100644 --- a/packages/core/src/app/payment/hostedCreditCard/withHostedCreditCardFieldset.tsx +++ b/packages/core/src/app/payment/hostedCreditCard/withHostedCreditCardFieldset.tsx @@ -1,19 +1,37 @@ -import { PaymentFormValues } from "@bigcommerce/checkout/payment-integration-api"; -import { CardInstrument, HostedFormOptions, Instrument, PaymentMethod } from '@bigcommerce/checkout-sdk'; +import { + CardInstrument, + HostedFormOptions, + Instrument, + PaymentMethod, +} from '@bigcommerce/checkout-sdk'; import { compact, forIn } from 'lodash'; -import React, { useCallback, useState, ComponentType, FunctionComponent, ReactNode } from 'react'; +import React, { ComponentType, FunctionComponent, ReactNode, useCallback, useState } from 'react'; import { ObjectSchema } from 'yup'; -import { withCheckout, CheckoutContextProps } from '../../checkout'; +import { PaymentFormValues } from '@bigcommerce/checkout/payment-integration-api'; + +import { CheckoutContextProps, withCheckout } from '../../checkout'; import { connectFormik, ConnectFormikProps } from '../../common/form'; import { MapToPropsFactory } from '../../common/hoc'; import { withLanguage, WithLanguageProps } from '../../locale'; import { withForm, WithFormProps } from '../../ui/form'; -import { getCreditCardInputStyles, CreditCardCustomerCodeField, CreditCardInputStylesType } from '../creditCard'; -import { isInstrumentCardCodeRequiredSelector, isInstrumentCardNumberRequiredSelector, isInstrumentFeatureAvailable } from '../storedInstrument'; +import { + CreditCardCustomerCodeField, + CreditCardInputStylesType, + getCreditCardInputStyles, +} from '../creditCard'; +import { + isInstrumentCardCodeRequiredSelector, + isInstrumentCardNumberRequiredSelector, + isInstrumentFeatureAvailable, +} from '../storedInstrument'; -import getHostedCreditCardValidationSchema, { HostedCreditCardValidationSchemaShape } from './getHostedCreditCardValidationSchema'; -import getHostedInstrumentValidationSchema, { HostedInstrumentValidationSchemaShape } from './getHostedInstrumentValidationSchema'; +import getHostedCreditCardValidationSchema, { + HostedCreditCardValidationSchemaShape, +} from './getHostedCreditCardValidationSchema'; +import getHostedInstrumentValidationSchema, { + HostedInstrumentValidationSchemaShape, +} from './getHostedInstrumentValidationSchema'; import HostedCreditCardFieldset from './HostedCreditCardFieldset'; import HostedCreditCardValidation from './HostedCreditCardValidation'; @@ -37,15 +55,17 @@ interface WithCheckoutContextProps { isInstrumentCardNumberRequired(instrument: Instrument): boolean; } -export default function withHostedCreditCardFieldset( - OriginalComponent: ComponentType> +export default function withHostedCreditCardFieldset< + TProps extends WithHostedCreditCardFieldsetProps, +>( + OriginalComponent: ComponentType>, ): ComponentType> { const Component: FunctionComponent< WithHostedCreditCardFieldsetProps & - WithCheckoutContextProps & - WithLanguageProps & - WithFormProps & - ConnectFormikProps + WithCheckoutContextProps & + WithLanguageProps & + WithFormProps & + ConnectFormikProps > = ({ formik: { setFieldValue, setFieldTouched, submitForm }, isCardCodeRequired, @@ -60,150 +80,211 @@ export default function withHostedCreditCardFieldset { const [focusedFieldType, setFocusedFieldType] = useState(); - const getHostedFieldId: (name: string) => string = useCallback(name => { - return `${compact([method.gateway, method.id]).join('-')}-${name}`; - }, [method]); - - const getHostedFormOptions: (selectedInstrument?: CardInstrument) => Promise = useCallback(async selectedInstrument => { - const styleProps = ['color', 'fontFamily', 'fontSize', 'fontWeight']; - const isInstrumentCardNumberRequired = selectedInstrument ? isInstrumentCardNumberRequiredProp(selectedInstrument) : false; - const isInstrumentCardCodeRequired = selectedInstrument ? isInstrumentCardCodeRequiredProp(selectedInstrument, method) : false; - const styleContainerId = selectedInstrument ? - (isInstrumentCardCodeRequired ? getHostedFieldId('ccCvv') : undefined) : - getHostedFieldId('ccNumber'); - - return { - fields: selectedInstrument ? - { - cardCodeVerification: isInstrumentCardCodeRequired && selectedInstrument ? - { - accessibilityLabel: language.translate('payment.credit_card_cvv_label'), - containerId: getHostedFieldId('ccCvv'), - instrumentId: selectedInstrument.bigpayToken, - } : - undefined, - cardNumberVerification: isInstrumentCardNumberRequired && selectedInstrument ? - { - accessibilityLabel: language.translate('payment.credit_card_number_label'), - containerId: getHostedFieldId('ccNumber'), - instrumentId: selectedInstrument.bigpayToken, - } : - undefined, - } : - { - cardCode: isCardCodeRequired ? - { - accessibilityLabel: language.translate('payment.credit_card_cvv_label'), - containerId: getHostedFieldId('ccCvv'), - } : - undefined, - cardExpiry: { - accessibilityLabel: language.translate('payment.credit_card_expiration_label'), - containerId: getHostedFieldId('ccExpiry'), - placeholder: language.translate('payment.credit_card_expiration_placeholder_text'), - }, - cardName: { - accessibilityLabel: language.translate('payment.credit_card_name_label'), - containerId: getHostedFieldId('ccName'), - }, - cardNumber: { - accessibilityLabel: language.translate('payment.credit_card_number_label'), - containerId: getHostedFieldId('ccNumber'), - }, + const getHostedFieldId: (name: string) => string = useCallback( + (name) => { + return `${compact([method.gateway, method.id]).join('-')}-${name}`; + }, + [method], + ); + + const getHostedFormOptions: ( + selectedInstrument?: CardInstrument, + ) => Promise = useCallback( + async (selectedInstrument) => { + const styleProps = ['color', 'fontFamily', 'fontSize', 'fontWeight']; + const isInstrumentCardNumberRequired = selectedInstrument + ? isInstrumentCardNumberRequiredProp(selectedInstrument) + : false; + const isInstrumentCardCodeRequired = selectedInstrument + ? isInstrumentCardCodeRequiredProp(selectedInstrument, method) + : false; + const styleContainerId = selectedInstrument + ? isInstrumentCardCodeRequired + ? getHostedFieldId('ccCvv') + : undefined + : getHostedFieldId('ccNumber'); + + return { + fields: selectedInstrument + ? { + cardCodeVerification: + isInstrumentCardCodeRequired && selectedInstrument + ? { + accessibilityLabel: language.translate( + 'payment.credit_card_cvv_label', + ), + containerId: getHostedFieldId('ccCvv'), + instrumentId: selectedInstrument.bigpayToken, + } + : undefined, + cardNumberVerification: + isInstrumentCardNumberRequired && selectedInstrument + ? { + accessibilityLabel: language.translate( + 'payment.credit_card_number_label', + ), + containerId: getHostedFieldId('ccNumber'), + instrumentId: selectedInstrument.bigpayToken, + } + : undefined, + } + : { + cardCode: isCardCodeRequired + ? { + accessibilityLabel: language.translate( + 'payment.credit_card_cvv_label', + ), + containerId: getHostedFieldId('ccCvv'), + } + : undefined, + cardExpiry: { + accessibilityLabel: language.translate( + 'payment.credit_card_expiration_label', + ), + containerId: getHostedFieldId('ccExpiry'), + placeholder: language.translate( + 'payment.credit_card_expiration_placeholder_text', + ), + }, + cardName: { + accessibilityLabel: language.translate( + 'payment.credit_card_name_label', + ), + containerId: getHostedFieldId('ccName'), + }, + cardNumber: { + accessibilityLabel: language.translate( + 'payment.credit_card_number_label', + ), + containerId: getHostedFieldId('ccNumber'), + }, + }, + styles: styleContainerId + ? { + default: await getCreditCardInputStyles(styleContainerId, styleProps), + error: await getCreditCardInputStyles( + styleContainerId, + styleProps, + CreditCardInputStylesType.Error, + ), + focus: await getCreditCardInputStyles( + styleContainerId, + styleProps, + CreditCardInputStylesType.Focus, + ), + } + : {}, + onBlur: ({ fieldType }) => { + if (focusedFieldType === fieldType) { + setFocusedFieldType(undefined); + } + }, + onCardTypeChange: ({ cardType }) => { + setFieldValue('hostedForm.cardType', cardType); + }, + onEnter: () => { + setSubmitted(true); + submitForm(); + }, + onFocus: ({ fieldType }) => { + setFocusedFieldType(fieldType); + }, + onValidate: ({ errors = {} }) => { + forIn(errors, (fieldErrors = [], fieldType) => { + const errorKey = `hostedForm.errors.${fieldType}`; + + setFieldValue(errorKey, fieldErrors[0]?.type ?? ''); + + if (fieldErrors[0]) { + setFieldTouched(errorKey); + } + }); }, - styles: styleContainerId ? - { - default: await getCreditCardInputStyles(styleContainerId, styleProps), - error: await getCreditCardInputStyles(styleContainerId, styleProps, CreditCardInputStylesType.Error), - focus: await getCreditCardInputStyles(styleContainerId, styleProps, CreditCardInputStylesType.Focus), - } : {}, - onBlur: ({ fieldType }) => { - if (focusedFieldType === fieldType) { - setFocusedFieldType(undefined); - } - }, - onCardTypeChange: ({ cardType }) => { - setFieldValue('hostedForm.cardType', cardType); - }, - onEnter: () => { - setSubmitted(true); - submitForm(); - }, - onFocus: ({ fieldType }) => { - setFocusedFieldType(fieldType); - }, - onValidate: ({ errors = {} }) => { - forIn(errors, (fieldErrors = [], fieldType) => { - const errorKey = `hostedForm.errors.${fieldType}`; - - setFieldValue(errorKey, fieldErrors[0]?.type ?? ''); - - if (fieldErrors[0]) { - setFieldTouched(errorKey); + }; + }, + [ + focusedFieldType, + getHostedFieldId, + isCardCodeRequired, + isInstrumentCardCodeRequiredProp, + isInstrumentCardNumberRequiredProp, + language, + method, + setFieldValue, + setFieldTouched, + setFocusedFieldType, + setSubmitted, + submitForm, + ], + ); + + const getHostedStoredCardValidationFieldset: ( + selectedInstrument: CardInstrument, + ) => ReactNode = useCallback( + (selectedInstrument) => { + const isInstrumentCardNumberRequired = selectedInstrument + ? isInstrumentCardNumberRequiredProp(selectedInstrument) + : false; + const isInstrumentCardCodeRequired = selectedInstrument + ? isInstrumentCardCodeRequiredProp(selectedInstrument, method) + : false; + + return ( + ReactNode = useCallback(selectedInstrument => { - const isInstrumentCardNumberRequired = selectedInstrument ? isInstrumentCardNumberRequiredProp(selectedInstrument) : false; - const isInstrumentCardCodeRequired = selectedInstrument ? isInstrumentCardCodeRequiredProp(selectedInstrument, method) : false; - - return ; - }, [ - focusedFieldType, - getHostedFieldId, - isInstrumentCardCodeRequiredProp, - isInstrumentCardNumberRequiredProp, - method, - ]); + cardNumberId={ + isInstrumentCardNumberRequired + ? getHostedFieldId('ccNumber') + : undefined + } + focusedFieldType={focusedFieldType} + /> + ); + }, + [ + focusedFieldType, + getHostedFieldId, + isInstrumentCardCodeRequiredProp, + isInstrumentCardNumberRequiredProp, + method, + ], + ); if (!method.config.isHostedFormEnabled) { - return ; + return ; } - return } - cardCodeId={ isCardCodeRequired ? getHostedFieldId('ccCvv') : undefined } - cardExpiryId={ getHostedFieldId('ccExpiry') } - cardNameId={ getHostedFieldId('ccName') } - cardNumberId={ getHostedFieldId('ccNumber') } - focusedFieldType={ focusedFieldType } - /> - } - hostedStoredCardValidationSchema={ getHostedInstrumentValidationSchema({ language }) } - hostedValidationSchema={ getHostedCreditCardValidationSchema({ language }) } - method={ method } - />; + return ( + + ) + } + cardCodeId={isCardCodeRequired ? getHostedFieldId('ccCvv') : undefined} + cardExpiryId={getHostedFieldId('ccExpiry')} + cardNameId={getHostedFieldId('ccName')} + cardNumberId={getHostedFieldId('ccNumber')} + focusedFieldType={focusedFieldType} + /> + } + hostedStoredCardValidationSchema={getHostedInstrumentValidationSchema({ language })} + hostedValidationSchema={getHostedCreditCardValidationSchema({ language })} + method={method} + /> + ); }; - return connectFormik(withForm(withLanguage(withCheckout(mapFromCheckoutProps)(Component)))) as ComponentType>; + return connectFormik( + withForm(withLanguage(withCheckout(mapFromCheckoutProps)(Component))), + ) as ComponentType>; } const mapFromCheckoutProps: MapToPropsFactory< @@ -213,10 +294,7 @@ const mapFromCheckoutProps: MapToPropsFactory< > = () => { return ({ checkoutState }, { isUsingMultiShipping = false, method }) => { const { - data: { - getConfig, - getCustomer, - }, + data: { getConfig, getCustomer }, } = checkoutState; const config = getConfig(); diff --git a/packages/core/src/app/payment/mapSubmitOrderErrorMessage.spec.ts b/packages/core/src/app/payment/mapSubmitOrderErrorMessage.spec.ts index 98799309a2..0174355a4f 100644 --- a/packages/core/src/app/payment/mapSubmitOrderErrorMessage.spec.ts +++ b/packages/core/src/app/payment/mapSubmitOrderErrorMessage.spec.ts @@ -18,7 +18,11 @@ describe('mapSubmitOrderErrorMessage()', () => { }); it('returns correct message when payment method is not valid', () => { - const message = mapSubmitOrderErrorMessage({ type: 'payment_method_invalid' }, translate, false); + const message = mapSubmitOrderErrorMessage( + { type: 'payment_method_invalid' }, + translate, + false, + ); expect(message).toEqual(translate('payment.payment_method_disabled_error')); }); @@ -38,9 +42,12 @@ describe('mapSubmitOrderErrorMessage()', () => { message: 'payment error message', }, translate, - false); + false, + ); - expect(message).toEqual(translate('payment.payment_method_error', { message: 'payment error message' })); + expect(message).toEqual( + translate('payment.payment_method_error', { message: 'payment error message' }), + ); }); it('returns correct message when payment error provider_fatal_error', () => { @@ -52,9 +59,12 @@ describe('mapSubmitOrderErrorMessage()', () => { message: 'payment error message', }, translate, - false); + false, + ); - expect(message).toEqual(translate('payment.payment_method_error', { message: 'payment error message' })); + expect(message).toEqual( + translate('payment.payment_method_error', { message: 'payment error message' }), + ); }); it('returns correct message when payment error payment_invalid', () => { @@ -66,9 +76,12 @@ describe('mapSubmitOrderErrorMessage()', () => { message: 'payment error message', }, translate, - false); + false, + ); - expect(message).toEqual(translate('payment.payment_method_error', { message: 'payment error message' })); + expect(message).toEqual( + translate('payment.payment_method_error', { message: 'payment error message' }), + ); }); it('returns correct message when payment error provider_error', () => { @@ -80,9 +93,12 @@ describe('mapSubmitOrderErrorMessage()', () => { message: 'payment error message', }, translate, - false); + false, + ); - expect(message).toEqual(translate('payment.payment_method_error', { message: 'payment error message' })); + expect(message).toEqual( + translate('payment.payment_method_error', { message: 'payment error message' }), + ); }); it('returns correct message when payment error provider_widget_error', () => { @@ -94,9 +110,12 @@ describe('mapSubmitOrderErrorMessage()', () => { message: 'payment error message', }, translate, - false); + false, + ); - expect(message).toEqual(translate('payment.payment_method_error', { message: 'payment error message' })); + expect(message).toEqual( + translate('payment.payment_method_error', { message: 'payment error message' }), + ); }); it('returns correct message when payment error user_payment_error', () => { @@ -108,13 +127,20 @@ describe('mapSubmitOrderErrorMessage()', () => { message: 'payment error message', }, translate, - false); + false, + ); - expect(message).toEqual(translate('payment.payment_method_error', { message: 'payment error message' })); + expect(message).toEqual( + translate('payment.payment_method_error', { message: 'payment error message' }), + ); }); it('returns correct message when tax provider is unavailable', () => { - const message = mapSubmitOrderErrorMessage({ type: 'tax_provider_unavailable' }, translate, false); + const message = mapSubmitOrderErrorMessage( + { type: 'tax_provider_unavailable' }, + translate, + false, + ); expect(message).toEqual(translate('payment.tax_provider_unavailable')); }); @@ -133,7 +159,8 @@ describe('mapSubmitOrderErrorMessage()', () => { }, }, translate, - true); + true, + ); expect(message).toEqual(translate('payment.errors.incorrect_address')); }); @@ -155,9 +182,14 @@ describe('mapSubmitOrderErrorMessage()', () => { }, }, translate, - true); - - expect(message).toEqual(`${translate('payment.errors.incorrect_address')} ${translate('payment.errors.incorrect_amount')}`); + true, + ); + + expect(message).toBe( + `${translate('payment.errors.incorrect_address')} ${translate( + 'payment.errors.incorrect_amount', + )}`, + ); }); it('returns untranslated error message when errors array is empty', () => { @@ -169,11 +201,13 @@ describe('mapSubmitOrderErrorMessage()', () => { message: 'bigpay error message', }, translate, - true); + true, + ); - expect(message).toEqual('bigpay error message'); + expect(message).toBe('bigpay error message'); }); }); + it('returns untranslated error message when bigpay request error and localization disabled', () => { const message = mapSubmitOrderErrorMessage( { @@ -192,9 +226,10 @@ describe('mapSubmitOrderErrorMessage()', () => { message: 'bigpay error message', }, translate, - false); + false, + ); - expect(message).toEqual('bigpay error message'); + expect(message).toBe('bigpay error message'); }); describe('When not bigpay request error and no error message exists,', () => { @@ -204,7 +239,8 @@ describe('mapSubmitOrderErrorMessage()', () => { type: 'unrecoverable', }, translate, - false); + false, + ); expect(message).toEqual(translate('common.unavailable_error')); }); @@ -215,7 +251,8 @@ describe('mapSubmitOrderErrorMessage()', () => { type: 'some_type', }, translate, - false); + false, + ); expect(message).toEqual(translate('payment.place_order_error')); }); @@ -238,7 +275,8 @@ describe('mapSubmitOrderErrorTitle()', () => { { type: 'unrecoverable', }, - translate); + translate, + ); expect(title).toEqual(translate('common.unavailable_heading')); }); @@ -248,7 +286,8 @@ describe('mapSubmitOrderErrorTitle()', () => { { type: 'some_type', }, - translate); + translate, + ); expect(title).toEqual(translate('common.error_heading')); }); diff --git a/packages/core/src/app/payment/mapSubmitOrderErrorMessage.ts b/packages/core/src/app/payment/mapSubmitOrderErrorMessage.ts index 4a29007b77..bbc3869918 100644 --- a/packages/core/src/app/payment/mapSubmitOrderErrorMessage.ts +++ b/packages/core/src/app/payment/mapSubmitOrderErrorMessage.ts @@ -4,7 +4,7 @@ import { includes } from 'lodash'; export default function mapSubmitOrderErrorMessage( error: any, translate: (key: string, data?: TranslationData) => string, - shouldLocalise: boolean + shouldLocalise: boolean, ): string { switch (error.type) { case 'not_initialized': @@ -26,19 +26,26 @@ export default function mapSubmitOrderErrorMessage( return translate('shipping.cart_change_error'); default: - if (includes([ - 'order_could_not_be_finalized_error', - 'provider_fatal_error', - 'payment_invalid', - 'provider_error', - 'provider_widget_error', - 'user_payment_error', - ], error.body && error.body.type)) { + if ( + includes( + [ + 'order_could_not_be_finalized_error', + 'provider_fatal_error', + 'payment_invalid', + 'provider_error', + 'provider_widget_error', + 'user_payment_error', + ], + error.body && error.body.type, + ) + ) { return translate('payment.payment_method_error', { message: error.message }); } if (shouldLocalise && error.body && error.body.errors && error.body.errors.length) { - const messages = error.body.errors.map((err: { code: any }) => translate(`payment.errors.${err.code}`)); + const messages = error.body.errors.map((err: { code: any }) => + translate(`payment.errors.${err.code}`), + ); return messages.join(' '); } @@ -47,15 +54,15 @@ export default function mapSubmitOrderErrorMessage( return error.message; } - return error.type === 'unrecoverable' ? - translate('common.unavailable_error') : - translate('payment.place_order_error'); + return error.type === 'unrecoverable' + ? translate('common.unavailable_error') + : translate('payment.place_order_error'); } } export function mapSubmitOrderErrorTitle( error: any, - translate: (key: string, data?: TranslationData) => string + translate: (key: string, data?: TranslationData) => string, ): string { if (error.type === 'unrecoverable') { return translate('common.unavailable_heading'); diff --git a/packages/core/src/app/payment/mapToOrderRequestBody.spec.ts b/packages/core/src/app/payment/mapToOrderRequestBody.spec.ts index e1a0e87027..aba23f11f3 100644 --- a/packages/core/src/app/payment/mapToOrderRequestBody.spec.ts +++ b/packages/core/src/app/payment/mapToOrderRequestBody.spec.ts @@ -2,76 +2,84 @@ import mapToOrderRequestBody from './mapToOrderRequestBody'; describe('mapToOrderRequestBody()', () => { it('transforms credit card form values into order payload', () => { - const result = mapToOrderRequestBody({ - ccCvv: '123', - ccExpiry: '12/23', - ccName: 'Big Shopper', - ccNumber: '4111 1111 1111 1111', - shouldSaveInstrument: true, - paymentProviderRadio: 'authorizenet', - }, true); + const result = mapToOrderRequestBody( + { + ccCvv: '123', + ccExpiry: '12/23', + ccName: 'Big Shopper', + ccNumber: '4111 1111 1111 1111', + shouldSaveInstrument: true, + paymentProviderRadio: 'authorizenet', + }, + true, + ); - expect(result) - .toEqual({ - payment: { - gatewayId: undefined, - methodId: 'authorizenet', - paymentData: { - ccCvv: '123', - ccExpiry: { - month: '12', - year: '2023', - }, - ccName: 'Big Shopper', - ccNumber: '4111111111111111', - shouldSaveInstrument: true, + expect(result).toEqual({ + payment: { + gatewayId: undefined, + methodId: 'authorizenet', + paymentData: { + ccCvv: '123', + ccExpiry: { + month: '12', + year: '2023', }, + ccName: 'Big Shopper', + ccNumber: '4111111111111111', + shouldSaveInstrument: true, }, - }); + }, + }); }); it('transforms stored instrument form values into order payload', () => { - const result = mapToOrderRequestBody({ - ccCvv: '123', - ccNumber: '4111 1111 1111 1111', - instrumentId: 'abc', - paymentProviderRadio: 'authorizenet', - }, true); + const result = mapToOrderRequestBody( + { + ccCvv: '123', + ccNumber: '4111 1111 1111 1111', + instrumentId: 'abc', + paymentProviderRadio: 'authorizenet', + }, + true, + ); - expect(result) - .toEqual({ - payment: { - gatewayId: undefined, - methodId: 'authorizenet', - paymentData: { - ccCvv: '123', - ccNumber: '4111111111111111', - instrumentId: 'abc', - }, + expect(result).toEqual({ + payment: { + gatewayId: undefined, + methodId: 'authorizenet', + paymentData: { + ccCvv: '123', + ccNumber: '4111111111111111', + instrumentId: 'abc', }, - }); + }, + }); }); it('transforms hosted / offsite / offline method form values into order payload', () => { - const result = mapToOrderRequestBody({ - paymentProviderRadio: 'adyen-paypal', - }, true); + const result = mapToOrderRequestBody( + { + paymentProviderRadio: 'adyen-paypal', + }, + true, + ); - expect(result) - .toEqual({ - payment: { - gatewayId: 'adyen', - methodId: 'paypal', - }, - }); + expect(result).toEqual({ + payment: { + gatewayId: 'adyen', + methodId: 'paypal', + }, + }); }); it('transforms form values into order payload for order that does not required additional payment details', () => { - const result = mapToOrderRequestBody({ - paymentProviderRadio: 'authorizenet', - }, false); + const result = mapToOrderRequestBody( + { + paymentProviderRadio: 'authorizenet', + }, + false, + ); - expect(result) - .toEqual({}); + expect(result).toEqual({}); }); }); diff --git a/packages/core/src/app/payment/mapToOrderRequestBody.ts b/packages/core/src/app/payment/mapToOrderRequestBody.ts index 177aef3414..6d27c0c1bb 100644 --- a/packages/core/src/app/payment/mapToOrderRequestBody.ts +++ b/packages/core/src/app/payment/mapToOrderRequestBody.ts @@ -1,14 +1,18 @@ -import { PaymentFormValues } from '@bigcommerce/checkout/payment-integration-api'; import { OrderPaymentRequestBody, OrderRequestBody } from '@bigcommerce/checkout-sdk'; import { isEmpty, isNil, omitBy } from 'lodash'; +import { PaymentFormValues } from '@bigcommerce/checkout/payment-integration-api'; + import { unformatCreditCardExpiryDate, unformatCreditCardNumber } from './creditCard'; -import { hasCreditCardExpiry, parseUniquePaymentMethodId } from './paymentMethod'; -import { hasCreditCardNumber } from './paymentMethod'; +import { + hasCreditCardExpiry, + hasCreditCardNumber, + parseUniquePaymentMethodId, +} from './paymentMethod'; export default function mapToOrderRequestBody( values: PaymentFormValues, - isPaymentDataRequired: boolean + isPaymentDataRequired: boolean, ): OrderRequestBody { if (!isPaymentDataRequired) { return {}; @@ -19,11 +23,18 @@ export default function mapToOrderRequestBody( const payload: OrderRequestBody = { payment: { gatewayId, methodId }, }; - const paymentData = omitBy({ - ...rest, - ccExpiry: hasCreditCardExpiry(values) ? unformatCreditCardExpiryDate(values.ccExpiry) : null, - ccNumber: hasCreditCardNumber(values) ? unformatCreditCardNumber(values.ccNumber) : null, - }, isNil) as OrderPaymentRequestBody['paymentData']; + const paymentData = omitBy( + { + ...rest, + ccExpiry: hasCreditCardExpiry(values) + ? unformatCreditCardExpiryDate(values.ccExpiry) + : null, + ccNumber: hasCreditCardNumber(values) + ? unformatCreditCardNumber(values.ccNumber) + : null, + }, + isNil, + ) as OrderPaymentRequestBody['paymentData']; if (payload.payment && !isEmpty(paymentData)) { payload.payment.paymentData = paymentData; diff --git a/packages/core/src/app/payment/payment-methods.mock.ts b/packages/core/src/app/payment/payment-methods.mock.ts index 323da7ec28..2b6c85d908 100644 --- a/packages/core/src/app/payment/payment-methods.mock.ts +++ b/packages/core/src/app/payment/payment-methods.mock.ts @@ -6,13 +6,9 @@ export function getPaymentMethod(): PaymentMethod { gateway: undefined, logoUrl: '', method: 'credit-card', - supportedCards: [ - 'VISA', - 'AMEX', - 'MC', - ], + supportedCards: ['VISA', 'AMEX', 'MC'], initializationData: { - payPalCreditProductBrandName: {credit: ''}, + payPalCreditProductBrandName: { credit: '' }, }, config: { displayName: 'Authorizenet', @@ -38,7 +34,7 @@ export function getPaypalCreditPaymentMethod(): PaymentMethod { logoUrl: '', method: 'paypal', initializationData: { - payPalCreditProductBrandName: {credit: 'Pay in 3'}, + payPalCreditProductBrandName: { credit: 'Pay in 3' }, }, type: 'PAYMENT_TYPE_API', }; @@ -50,11 +46,7 @@ export function getMobilePaymentMethod(): PaymentMethod { gateway: undefined, logoUrl: '', method: 'credit-card', - supportedCards: [ - 'VISA', - 'AMEX', - 'MC', - ], + supportedCards: ['VISA', 'AMEX', 'MC'], initializationData: { showOnlyOnMobileDevices: true, }, diff --git a/packages/core/src/app/payment/paymentMethod/AdyenV2CardValidation.spec.tsx b/packages/core/src/app/payment/paymentMethod/AdyenV2CardValidation.spec.tsx index 39859fe6b0..25c1205221 100644 --- a/packages/core/src/app/payment/paymentMethod/AdyenV2CardValidation.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/AdyenV2CardValidation.spec.tsx @@ -2,18 +2,17 @@ import { AdyenV2ValidationState } from '@bigcommerce/checkout-sdk'; import { mount } from 'enzyme'; import React, { FunctionComponent } from 'react'; -import AdyenV2CardValidation, { AdyenV2CardValidationProps } from './AdyenV2CardValidation'; import PaymentContext, { PaymentContextProps } from '../PaymentContext'; +import AdyenV2CardValidation, { AdyenV2CardValidationProps } from './AdyenV2CardValidation'; + describe('AdyenV2CardValidation', () => { let defaultProps: AdyenV2CardValidationProps; let paymentContext: PaymentContextProps; let AdyenV2CardValidationTest: FunctionComponent; beforeEach(() => { - AdyenV2CardValidationTest = props => ( - - ); + AdyenV2CardValidationTest = (props) => ; }); it('renders Adyen V2 Card Number and CVV fields', () => { @@ -25,7 +24,7 @@ describe('AdyenV2CardValidation', () => { verificationFieldsContainerId: 'container', }; - const container = mount(); + const container = mount(); const field = container.find('[id="encryptedSecurityCode"]'); @@ -41,7 +40,8 @@ describe('AdyenV2CardValidation', () => { shouldShowNumberField: true, verificationFieldsContainerId: 'container', }; - const container = mount(); + + const container = mount(); const field = container.find('[id="encryptedExpiryDate"]'); @@ -58,7 +58,7 @@ describe('AdyenV2CardValidation', () => { cardValidationState: {} as AdyenV2ValidationState, }; - const container = mount(); + const container = mount(); const field = container.find('[id="encryptedSecurityCode"]'); @@ -78,7 +78,7 @@ describe('AdyenV2CardValidation', () => { } as AdyenV2ValidationState, }; - const container = mount(); + const container = mount(); const field = container.find('[id="encryptedSecurityCode"]'); @@ -89,7 +89,7 @@ describe('AdyenV2CardValidation', () => { beforeEach(() => { defaultProps = { paymentMethod: { - method: 'scheme' + method: 'scheme', }, shouldShowNumberField: true, verificationFieldsContainerId: 'container', @@ -112,7 +112,7 @@ describe('AdyenV2CardValidation', () => { it('should render error when entered last 4 symbols is not equal to the last 4 from selected card', () => { defaultProps = { paymentMethod: { - method: 'scheme' + method: 'scheme', }, shouldShowNumberField: true, verificationFieldsContainerId: 'container', @@ -131,7 +131,8 @@ describe('AdyenV2CardValidation', () => { }, }; - const container = mount(); + const container = mount(); + container.setProps({ cardValidationState: { blob: 'adyenjs_', @@ -142,14 +143,16 @@ describe('AdyenV2CardValidation', () => { }, }); container.update(); + const field = container.find('[id="encryptedCardNumber"]'); - + expect(field.hasClass('adyen-checkout__input--error')).toBeTruthy(); expect(field).toHaveLength(1); }); it('should NOT render error when entered last 4 symbols is equal to the last 4 from selected card', () => { - const container = mount(); + const container = mount(); + container.setProps({ cardValidationState: { blob: 'adyenjs_', @@ -174,14 +177,17 @@ describe('AdyenV2CardValidation', () => { setValidationSchema: jest.fn(), hidePaymentSubmitButton: jest.fn(), }; - + mount( - - - + + + , + ); + + expect(paymentContext.disableSubmit).toHaveBeenCalledWith( + defaultProps.paymentMethod, + true, ); - - expect(paymentContext.disableSubmit).toHaveBeenCalledWith(defaultProps.paymentMethod, true); }); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/AdyenV2CardValidation.tsx b/packages/core/src/app/payment/paymentMethod/AdyenV2CardValidation.tsx index 4e46e7f9b2..fe95dac7f4 100644 --- a/packages/core/src/app/payment/paymentMethod/AdyenV2CardValidation.tsx +++ b/packages/core/src/app/payment/paymentMethod/AdyenV2CardValidation.tsx @@ -1,11 +1,11 @@ import { AdyenV2ValidationState, CardInstrument, PaymentMethod } from '@bigcommerce/checkout-sdk'; import classNames from 'classnames'; -import React, { useContext, useEffect, useState, FunctionComponent } from 'react'; +import React, { FunctionComponent, useContext, useEffect, useState } from 'react'; import { TranslatedString } from '../../locale'; import PaymentContext from '../PaymentContext'; -export type FieldsValidation = {[key in AdyenV2CardFields]?: AdyenV2ValidationState}; +export type FieldsValidation = { [key in AdyenV2CardFields]?: AdyenV2ValidationState }; enum AdyenV2CardFields { CardNumber = 'encryptedCardNumber', @@ -21,22 +21,32 @@ export interface AdyenV2CardValidationProps { selectedInstrument?: CardInstrument; } -const getInitialValidationState = ({ shouldShowNumberField, method }: {shouldShowNumberField: boolean, method: string }) => { - const validationState: FieldsValidation = {} +const getInitialValidationState = ({ + shouldShowNumberField, + method, +}: { + shouldShowNumberField: boolean; + method: string; +}) => { + const validationState: FieldsValidation = {}; + if (shouldShowNumberField) { validationState[AdyenV2CardFields.CardNumber] = { valid: false }; } + if (method === 'scheme') { validationState[AdyenV2CardFields.SecurityCode] = { valid: false }; } + if (method === 'bcmc') { validationState[AdyenV2CardFields.ExpiryDate] = { valid: false }; } - return validationState; -} -const isFieldInvalid = (fieldKey: AdyenV2CardFields, fieldsValidation: FieldsValidation): boolean => !!fieldsValidation[fieldKey] && !fieldsValidation[fieldKey]?.valid; + return validationState; +}; +const isFieldInvalid = (fieldKey: AdyenV2CardFields, fieldsValidation: FieldsValidation): boolean => + !!fieldsValidation[fieldKey] && !fieldsValidation[fieldKey]?.valid; const AdyenV2CardValidation: FunctionComponent = ({ verificationFieldsContainerId, @@ -47,124 +57,169 @@ const AdyenV2CardValidation: FunctionComponent = ({ }) => { const paymentContext = useContext(PaymentContext); - const [ fieldsValidation, setFieldsValidation ] = useState(getInitialValidationState({shouldShowNumberField, method: paymentMethod.method})); + const [fieldsValidation, setFieldsValidation] = useState( + getInitialValidationState({ shouldShowNumberField, method: paymentMethod.method }), + ); useEffect(() => { if (!cardValidationState) { return; } - if (cardValidationState.fieldType) { if (cardValidationState.fieldType === AdyenV2CardFields.CardNumber) { setFieldsValidation({ ...fieldsValidation, - [AdyenV2CardFields.CardNumber]: cardValidationState?.endDigits !== selectedInstrument?.last4 ? - { ...cardValidationState, valid: false } : - { ...cardValidationState }, - }) + [AdyenV2CardFields.CardNumber]: + cardValidationState.endDigits !== selectedInstrument?.last4 + ? { ...cardValidationState, valid: false } + : { ...cardValidationState }, + }); } else { setFieldsValidation({ ...fieldsValidation, [cardValidationState.fieldType]: cardValidationState, - }) + }); } } - }, [cardValidationState, setFieldsValidation]); useEffect(() => { - paymentContext?.disableSubmit(paymentMethod, Object.values(fieldsValidation).some(field => !field?.valid)); + paymentContext?.disableSubmit( + paymentMethod, + Object.values(fieldsValidation).some((field) => !field.valid), + ); }, [fieldsValidation, paymentContext, paymentMethod]); useEffect(() => { if (selectedInstrument?.bigpayToken) { - setFieldsValidation(getInitialValidationState({shouldShowNumberField, method: paymentMethod.method})); + setFieldsValidation( + getInitialValidationState({ shouldShowNumberField, method: paymentMethod.method }), + ); } else { paymentContext?.disableSubmit(paymentMethod, false); } - }, [getInitialValidationState, selectedInstrument, paymentContext, paymentMethod, shouldShowNumberField]); - - - const showValidationIcon = (key: AdyenV2CardFields) => isFieldInvalid(key, fieldsValidation) && ( - - - - ); + }, [ + getInitialValidationState, + selectedInstrument, + paymentContext, + paymentMethod, + shouldShowNumberField, + ]); + + const showValidationIcon = (key: AdyenV2CardFields) => + isFieldInvalid(key, fieldsValidation) && ( + + + + ); return (
    - { shouldShowNumberField &&

    - - - - -
    - - -

    } - -
    - { shouldShowNumberField &&
    - -
    - { showValidationIcon(AdyenV2CardFields.CardNumber) } -
    } - - { paymentMethod.method === 'scheme' &&
    - -
    - { showValidationIcon(AdyenV2CardFields.SecurityCode) } -
    } - { paymentMethod.method === 'bcmc' &&
    - + {shouldShowNumberField && ( +

    + + + + +
    + + +

    + )} + +
    + {shouldShowNumberField && (
    - { showValidationIcon(AdyenV2CardFields.ExpiryDate) } -
    } + className={classNames( + 'form-field', + 'form-field--ccNumber', + { + 'form-field--ccNumber--hasExpiryDate': + paymentMethod.method === 'bcmc', + }, + // This div is hiding by CSS because there is an Adyen library in + // checkout-sdk which mounts verification fields and if is removed with JS this mounting event will be thrown an error + { 'form-field-ccNumber--hide': !shouldShowNumberField }, + )} + > + +
    + {showValidationIcon(AdyenV2CardFields.CardNumber)} +
    + )} + + {paymentMethod.method === 'scheme' && ( +
    + +
    + {showValidationIcon(AdyenV2CardFields.SecurityCode)} +
    + )} + {paymentMethod.method === 'bcmc' && ( +
    + +
    + {showValidationIcon(AdyenV2CardFields.ExpiryDate)} +
    + )}
    ); diff --git a/packages/core/src/app/payment/paymentMethod/AdyenV2PaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/AdyenV2PaymentMethod.spec.tsx index 779828e14b..05c27c1227 100644 --- a/packages/core/src/app/payment/paymentMethod/AdyenV2PaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/AdyenV2PaymentMethod.spec.tsx @@ -1,4 +1,9 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService, PaymentMethod } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, + PaymentMethod, +} from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import { Formik } from 'formik'; import { noop } from 'lodash'; @@ -12,7 +17,9 @@ import { Modal, ModalProps } from '../../ui/modal'; import { getPaymentMethod } from '../payment-methods.mock'; import { AdyenPaymentMethodProps } from './AdyenV2PaymentMethod'; -import HostedWidgetPaymentMethod, { HostedWidgetPaymentMethodProps } from './HostedWidgetPaymentMethod'; +import HostedWidgetPaymentMethod, { + HostedWidgetPaymentMethodProps, +} from './HostedWidgetPaymentMethod'; import { default as PaymentMethodComponent, PaymentMethodProps } from './PaymentMethod'; describe('when using Adyen V2 payment', () => { @@ -34,23 +41,17 @@ describe('when using Adyen V2 payment', () => { localeContext = createLocaleContext(getStoreConfig()); method = { ...getPaymentMethod(), id: 'scheme', gateway: 'adyenv2', method: 'scheme' }; - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - jest.spyOn(checkoutService, 'deinitializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'deinitializePayment').mockResolvedValue(checkoutState); - jest.spyOn(checkoutService, 'initializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'initializePayment').mockResolvedValue(checkoutState); - PaymentMethodTest = props => ( - - - - + PaymentMethodTest = (props) => ( + + + + @@ -58,21 +59,24 @@ describe('when using Adyen V2 payment', () => { }); it('renders as hosted widget method', () => { - const container = mount(); - const component: ReactWrapper = container.find(HostedWidgetPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(HostedWidgetPaymentMethod); - expect(component.props()) - .toEqual(expect.objectContaining({ + expect(component.props()).toEqual( + expect.objectContaining({ containerId: 'adyen-scheme-component-field', deinitializePayment: expect.any(Function), initializePayment: expect.any(Function), method, - })); + }), + ); }); it('initializes method with required config', () => { - const container = mount(); - const component: ReactWrapper = container.find(HostedWidgetPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(HostedWidgetPaymentMethod); component.prop('initializePayment')({ methodId: method.id, @@ -81,8 +85,8 @@ describe('when using Adyen V2 payment', () => { expect(checkoutService.initializePayment).toHaveBeenCalled(); - expect(checkoutService.initializePayment) - .toHaveBeenCalledWith(expect.objectContaining({ + expect(checkoutService.initializePayment).toHaveBeenCalledWith( + expect.objectContaining({ adyenv2: { cardVerificationContainerId: undefined, containerId: 'adyen-scheme-component-field', @@ -103,7 +107,8 @@ describe('when using Adyen V2 payment', () => { }, gatewayId: method.gateway, methodId: method.id, - })); + }), + ); }); describe('#During payment', () => { @@ -115,31 +120,33 @@ describe('when using Adyen V2 payment', () => { method: getPaymentMethod(), onUnhandledError: jest.fn(), }; - const container = mount(); - const component: ReactWrapper = container.find(HostedWidgetPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(HostedWidgetPaymentMethod); component.prop('initializePayment')({ methodId: method.id, gatewayId: method.gateway, }); - const initializeOptions = (defaultAdyenProps.initializePayment as jest.Mock).mock.calls[0][0]; + const initializeOptions = (defaultAdyenProps.initializePayment as jest.Mock).mock + .calls[0][0]; act(() => { initializeOptions.adyenv2.additionalActionOptions.onBeforeLoad(true); }); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); act(() => { container.update(); }); - expect(container.find(Modal).prop('isOpen')) - .toEqual(true); + expect(container.find(Modal).prop('isOpen')).toBe(true); - expect(container.find('#adyen-scheme-additional-action-component-field')) - .toHaveLength(1); + expect(container.find('#adyen-scheme-additional-action-component-field')).toHaveLength( + 1, + ); }); it('Do not render 3DS modal if required by selected method', async () => { @@ -150,31 +157,33 @@ describe('when using Adyen V2 payment', () => { method: getPaymentMethod(), onUnhandledError: jest.fn(), }; - const container = mount(); - const component: ReactWrapper = container.find(HostedWidgetPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(HostedWidgetPaymentMethod); component.prop('initializePayment')({ methodId: method.id, gatewayId: method.gateway, }); - const initializeOptions = (defaultAdyenProps.initializePayment as jest.Mock).mock.calls[0][0]; + const initializeOptions = (defaultAdyenProps.initializePayment as jest.Mock).mock + .calls[0][0]; act(() => { initializeOptions.adyenv2.additionalActionOptions.onBeforeLoad(false); }); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); act(() => { container.update(); }); - expect(container.find(Modal).prop('isOpen')) - .toEqual(false); + expect(container.find(Modal).prop('isOpen')).toBe(false); - expect(container.find('#adyen-scheme-additional-action-component-field')) - .toHaveLength(1); + expect(container.find('#adyen-scheme-additional-action-component-field')).toHaveLength( + 1, + ); }); it('cancels 3DS modal flow if user chooses to close modal', async () => { @@ -186,21 +195,26 @@ describe('when using Adyen V2 payment', () => { method: getPaymentMethod(), onUnhandledError: jest.fn(), }; - const container = mount(); - const component: ReactWrapper = container.find(HostedWidgetPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(HostedWidgetPaymentMethod); component.prop('initializePayment')({ methodId: method.id, gatewayId: method.gateway, }); - const initializeOptions = (defaultAdyenProps.initializePayment as jest.Mock).mock.calls[0][0]; + const initializeOptions = (defaultAdyenProps.initializePayment as jest.Mock).mock + .calls[0][0]; act(() => { - initializeOptions.adyenv2.additionalActionOptions.onLoad(cancelAdditionalActionModalFlow, true); + initializeOptions.adyenv2.additionalActionOptions.onLoad( + cancelAdditionalActionModalFlow, + true, + ); }); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); act(() => { container.update(); diff --git a/packages/core/src/app/payment/paymentMethod/AdyenV2PaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/AdyenV2PaymentMethod.tsx index 36916ddb81..7cc5ef0aef 100644 --- a/packages/core/src/app/payment/paymentMethod/AdyenV2PaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/AdyenV2PaymentMethod.tsx @@ -1,15 +1,25 @@ -import { AdyenCreditCardComponentOptions, AdyenIdealComponentOptions, AdyenV2ValidationState, CardInstrument } from '@bigcommerce/checkout-sdk'; +import { + AdyenCreditCardComponentOptions, + AdyenIdealComponentOptions, + AdyenV2ValidationState, + CardInstrument, +} from '@bigcommerce/checkout-sdk'; import _ from 'lodash'; -import React, { useCallback, useRef, useState, FunctionComponent } from 'react'; +import React, { FunctionComponent, useCallback, useRef, useState } from 'react'; import { Omit } from 'utility-types'; import { TranslatedString } from '../../locale'; import { Modal } from '../../ui/modal'; import AdyenV2CardValidation from './AdyenV2CardValidation'; -import HostedWidgetPaymentMethod, { HostedWidgetPaymentMethodProps } from './HostedWidgetPaymentMethod'; +import HostedWidgetPaymentMethod, { + HostedWidgetPaymentMethodProps, +} from './HostedWidgetPaymentMethod'; -export type AdyenPaymentMethodProps = Omit; +export type AdyenPaymentMethodProps = Omit< + HostedWidgetPaymentMethodProps, + 'containerId' | 'hideContentWhenSignedOut' +>; export interface AdyenOptions { scheme: AdyenCreditCardComponentOptions; @@ -59,7 +69,7 @@ const AdyenV2PaymentMethod: FunctionComponent = ({ }, }; - const onBeforeLoad = useCallback((shopperInteraction: boolean) => { + const onBeforeLoad = useCallback((shopperInteraction: boolean) => { ref.current.shouldShowModal = shopperInteraction; if (ref.current.shouldShowModal) { @@ -87,81 +97,104 @@ const AdyenV2PaymentMethod: FunctionComponent = ({ } }, []); - const initializeAdyenPayment: HostedWidgetPaymentMethodProps['initializePayment'] = useCallback((options, selectedInstrument) => { - const selectedInstrumentId = selectedInstrument?.bigpayToken; - - return initializePayment({ - ...options, - adyenv2: { - cardVerificationContainerId: selectedInstrumentId && cardVerificationContainerId, - containerId, - hasVaultedInstruments: !!selectedInstrumentId, - options: adyenOptions[component], - threeDS2ContainerId, - additionalActionOptions: { - widgetSize: '05', - containerId: additionalActionContainerId, - onBeforeLoad, - onComplete, - onLoad, + const initializeAdyenPayment: HostedWidgetPaymentMethodProps['initializePayment'] = useCallback( + (options, selectedInstrument) => { + const selectedInstrumentId = selectedInstrument?.bigpayToken; + + return initializePayment({ + ...options, + adyenv2: { + cardVerificationContainerId: + selectedInstrumentId && cardVerificationContainerId, + containerId, + hasVaultedInstruments: !!selectedInstrumentId, + options: adyenOptions[component], + threeDS2ContainerId, + additionalActionOptions: { + widgetSize: '05', + containerId: additionalActionContainerId, + onBeforeLoad, + onComplete, + onLoad, + }, + shouldShowNumberField: ref.current.shouldShowNumberField, + validateCardFields: (state: AdyenV2ValidationState) => { + setCardValidationState(state); + }, }, - shouldShowNumberField: ref.current.shouldShowNumberField, - validateCardFields: (state: AdyenV2ValidationState) => { setCardValidationState(state); }, - }, - }); - }, [initializePayment, component, cardVerificationContainerId, containerId, additionalActionContainerId, threeDS2ContainerId, adyenOptions, onBeforeLoad, onComplete, onLoad]); - - const validateInstrument = (shouldShowNumberField: boolean, selectedInstrument: CardInstrument) => { + }); + }, + [ + initializePayment, + component, + cardVerificationContainerId, + containerId, + additionalActionContainerId, + threeDS2ContainerId, + adyenOptions, + onBeforeLoad, + onComplete, + onLoad, + ], + ); + + const validateInstrument = ( + shouldShowNumberField: boolean, + selectedInstrument: CardInstrument, + ) => { ref.current.shouldShowNumberField = shouldShowNumberField; - return ; + return ( + + ); }; const isAccountInstrument = () => { switch (method.method) { - case 'directEbanking': - case 'giropay': - case 'ideal': - case 'sepadirectdebit': - return true; - default: - return false; + case 'directEbanking': + case 'giropay': + case 'ideal': + case 'sepadirectdebit': + return true; + + default: + return false; } }; - return <> - - - } - isOpen={ showAdditionalActionContent } - onRequestClose={ cancelAdditionalActionModalFlow } - shouldShowCloseButton={ true } - > -
    - - { !showAdditionalActionContent && -
    } - ; + return ( + <> + + + } + isOpen={showAdditionalActionContent} + onRequestClose={cancelAdditionalActionModalFlow} + shouldShowCloseButton={true} + > +
    + + {!showAdditionalActionContent && ( +
    + )} + + ); }; export default AdyenV2PaymentMethod; diff --git a/packages/core/src/app/payment/paymentMethod/AdyenV3CardValidation.spec.tsx b/packages/core/src/app/payment/paymentMethod/AdyenV3CardValidation.spec.tsx index 9bded06ce9..d335d98e54 100644 --- a/packages/core/src/app/payment/paymentMethod/AdyenV3CardValidation.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/AdyenV3CardValidation.spec.tsx @@ -2,30 +2,29 @@ import { AdyenV3ValidationState } from '@bigcommerce/checkout-sdk'; import { mount } from 'enzyme'; import React, { FunctionComponent } from 'react'; -import AdyenV3CardValidation, { AdyenV3CardValidationProps } from './AdyenV3CardValidation'; import PaymentContext, { PaymentContextProps } from '../PaymentContext'; +import AdyenV3CardValidation, { AdyenV3CardValidationProps } from './AdyenV3CardValidation'; + describe('AdyenV3CardValidation', () => { let defaultProps: AdyenV3CardValidationProps; let paymentContext: PaymentContextProps; let AdyenV3CardValidationTest: FunctionComponent; beforeEach(() => { - AdyenV3CardValidationTest = props => ( - - ); + AdyenV3CardValidationTest = (props) => ; }); it('renders Adyen V3 Card Number and CVV fields', () => { defaultProps = { paymentMethod: { - method: 'scheme' + method: 'scheme', }, shouldShowNumberField: true, verificationFieldsContainerId: 'container', }; - const container = mount(); + const container = mount(); const field = container.find('[id="encryptedSecurityCode"]'); @@ -41,7 +40,8 @@ describe('AdyenV3CardValidation', () => { shouldShowNumberField: true, verificationFieldsContainerId: 'container', }; - const container = mount(); + + const container = mount(); const field = container.find('[id="encryptedExpiryDate"]'); @@ -51,14 +51,14 @@ describe('AdyenV3CardValidation', () => { it('render with empty required fields', () => { defaultProps = { paymentMethod: { - method: 'scheme' + method: 'scheme', }, shouldShowNumberField: false, verificationFieldsContainerId: 'container', cardValidationState: {} as AdyenV3ValidationState, }; - const container = mount(); + const container = mount(); const field = container.find('[id="encryptedSecurityCode"]'); @@ -68,7 +68,7 @@ describe('AdyenV3CardValidation', () => { it('render with invalid fields', () => { defaultProps = { paymentMethod: { - method: 'scheme' + method: 'scheme', }, shouldShowNumberField: false, verificationFieldsContainerId: 'container', @@ -78,7 +78,7 @@ describe('AdyenV3CardValidation', () => { } as AdyenV3ValidationState, }; - const container = mount(); + const container = mount(); const field = container.find('[id="encryptedSecurityCode"]'); @@ -89,7 +89,7 @@ describe('AdyenV3CardValidation', () => { beforeEach(() => { defaultProps = { paymentMethod: { - method: 'scheme' + method: 'scheme', }, shouldShowNumberField: true, verificationFieldsContainerId: 'container', @@ -112,7 +112,7 @@ describe('AdyenV3CardValidation', () => { it('should render error when entered last 4 symbols is not equal to the last 4 from selected card', () => { defaultProps = { paymentMethod: { - method: 'scheme' + method: 'scheme', }, shouldShowNumberField: true, verificationFieldsContainerId: 'container', @@ -131,7 +131,8 @@ describe('AdyenV3CardValidation', () => { }, }; - const container = mount(); + const container = mount(); + container.setProps({ cardValidationState: { blob: 'adyenjs_', @@ -142,14 +143,16 @@ describe('AdyenV3CardValidation', () => { }, }); container.update(); + const field = container.find('[id="encryptedCardNumber"]'); - + expect(field.hasClass('adyen-checkout__input--error')).toBeTruthy(); expect(field).toHaveLength(1); }); it('should NOT render error when entered last 4 symbols is equal to the last 4 from selected card', () => { - const container = mount(); + const container = mount(); + container.setProps({ cardValidationState: { blob: 'adyenjs_', @@ -174,14 +177,17 @@ describe('AdyenV3CardValidation', () => { setValidationSchema: jest.fn(), hidePaymentSubmitButton: jest.fn(), }; - + mount( - - - + + + , + ); + + expect(paymentContext.disableSubmit).toHaveBeenCalledWith( + defaultProps.paymentMethod, + true, ); - - expect(paymentContext.disableSubmit).toHaveBeenCalledWith(defaultProps.paymentMethod, true); }); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/AdyenV3CardValidation.tsx b/packages/core/src/app/payment/paymentMethod/AdyenV3CardValidation.tsx index cfdea07362..c41750e947 100644 --- a/packages/core/src/app/payment/paymentMethod/AdyenV3CardValidation.tsx +++ b/packages/core/src/app/payment/paymentMethod/AdyenV3CardValidation.tsx @@ -1,11 +1,11 @@ import { AdyenV3ValidationState, CardInstrument, PaymentMethod } from '@bigcommerce/checkout-sdk'; import classNames from 'classnames'; -import React, { useContext, useEffect, useState, FunctionComponent } from 'react'; +import React, { FunctionComponent, useContext, useEffect, useState } from 'react'; import { TranslatedString } from '../../locale'; import PaymentContext from '../PaymentContext'; -export type FieldsValidation = {[key in AdyenV3CardFields]?: AdyenV3ValidationState}; +export type FieldsValidation = { [key in AdyenV3CardFields]?: AdyenV3ValidationState }; enum AdyenV3CardFields { CardNumber = 'encryptedCardNumber', @@ -21,21 +21,32 @@ export interface AdyenV3CardValidationProps { selectedInstrument?: CardInstrument; } -const getInitialValidationState = ({ shouldShowNumberField, method }: {shouldShowNumberField: boolean, method: string }) => { - const validationState: FieldsValidation = {} +const getInitialValidationState = ({ + shouldShowNumberField, + method, +}: { + shouldShowNumberField: boolean; + method: string; +}) => { + const validationState: FieldsValidation = {}; + if (shouldShowNumberField) { validationState[AdyenV3CardFields.CardNumber] = { valid: false }; } + if (method === 'scheme') { validationState[AdyenV3CardFields.SecurityCode] = { valid: false }; } + if (method === 'bcmc') { validationState[AdyenV3CardFields.ExpiryDate] = { valid: false }; } + return validationState; -} +}; -const isFieldInvalid = (fieldKey: AdyenV3CardFields, fieldsValidation: FieldsValidation): boolean => !!fieldsValidation[fieldKey] && !fieldsValidation[fieldKey]?.valid; +const isFieldInvalid = (fieldKey: AdyenV3CardFields, fieldsValidation: FieldsValidation): boolean => + !!fieldsValidation[fieldKey] && !fieldsValidation[fieldKey]?.valid; const AdyenV3CardValidation: FunctionComponent = ({ verificationFieldsContainerId, @@ -46,123 +57,168 @@ const AdyenV3CardValidation: FunctionComponent = ({ }) => { const paymentContext = useContext(PaymentContext); - const [ fieldsValidation, setFieldsValidation ] = useState(getInitialValidationState({shouldShowNumberField, method: paymentMethod.method})); + const [fieldsValidation, setFieldsValidation] = useState( + getInitialValidationState({ shouldShowNumberField, method: paymentMethod.method }), + ); useEffect(() => { if (!cardValidationState) { return; } - if (cardValidationState.fieldType) { if (cardValidationState.fieldType === AdyenV3CardFields.CardNumber) { setFieldsValidation({ ...fieldsValidation, - [AdyenV3CardFields.CardNumber]: cardValidationState?.endDigits !== selectedInstrument?.last4 ? - { ...cardValidationState, valid: false } : - { ...cardValidationState }, - }) + [AdyenV3CardFields.CardNumber]: + cardValidationState.endDigits !== selectedInstrument?.last4 + ? { ...cardValidationState, valid: false } + : { ...cardValidationState }, + }); } else { setFieldsValidation({ ...fieldsValidation, [cardValidationState.fieldType]: cardValidationState, - }) + }); } } - }, [cardValidationState, setFieldsValidation]); useEffect(() => { - paymentContext?.disableSubmit(paymentMethod, Object.values(fieldsValidation).some(field => !field?.valid)); + paymentContext?.disableSubmit( + paymentMethod, + Object.values(fieldsValidation).some((field) => !field.valid), + ); }, [fieldsValidation, paymentContext, paymentMethod]); useEffect(() => { if (selectedInstrument?.bigpayToken) { - setFieldsValidation(getInitialValidationState({shouldShowNumberField, method: paymentMethod.method})); + setFieldsValidation( + getInitialValidationState({ shouldShowNumberField, method: paymentMethod.method }), + ); } else { paymentContext?.disableSubmit(paymentMethod, false); } - }, [selectedInstrument, paymentContext, paymentMethod, getInitialValidationState, shouldShowNumberField]); - - - const showValidationIcon = (key: AdyenV3CardFields) => isFieldInvalid(key, fieldsValidation) && ( - - - - ); + }, [ + selectedInstrument, + paymentContext, + paymentMethod, + getInitialValidationState, + shouldShowNumberField, + ]); + + const showValidationIcon = (key: AdyenV3CardFields) => + isFieldInvalid(key, fieldsValidation) && ( + + + + ); return (
    - { shouldShowNumberField &&

    - - - - -
    - - -

    } - -
    - { shouldShowNumberField &&
    - -
    - { showValidationIcon(AdyenV3CardFields.CardNumber) } -
    } - { paymentMethod.method === 'scheme' &&
    - -
    - { showValidationIcon(AdyenV3CardFields.SecurityCode) } -
    } - { paymentMethod.method === 'bcmc' &&
    - + {shouldShowNumberField && ( +

    + + + + +
    + + +

    + )} + +
    + {shouldShowNumberField && (
    - { showValidationIcon(AdyenV3CardFields.ExpiryDate) } -
    } + className={classNames( + 'form-field', + 'form-field--ccNumber', + { + 'form-field--ccNumber--hasExpiryDate': + paymentMethod.method === 'bcmc', + }, + // This div is hiding by CSS because there is an Adyen library in + // checkout-sdk which mounts verification fields and if is removed with JS this mounting event will be thrown an error + { 'form-field-ccNumber--hide': !shouldShowNumberField }, + )} + > + +
    + {showValidationIcon(AdyenV3CardFields.CardNumber)} +
    + )} + {paymentMethod.method === 'scheme' && ( +
    + +
    + {showValidationIcon(AdyenV3CardFields.SecurityCode)} +
    + )} + {paymentMethod.method === 'bcmc' && ( +
    + +
    + {showValidationIcon(AdyenV3CardFields.ExpiryDate)} +
    + )}
    ); diff --git a/packages/core/src/app/payment/paymentMethod/AdyenV3PaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/AdyenV3PaymentMethod.spec.tsx index ecf8b6674c..ca98374efc 100644 --- a/packages/core/src/app/payment/paymentMethod/AdyenV3PaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/AdyenV3PaymentMethod.spec.tsx @@ -1,4 +1,9 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService, PaymentMethod } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, + PaymentMethod, +} from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import { Formik } from 'formik'; import { noop } from 'lodash'; @@ -12,7 +17,9 @@ import { Modal, ModalProps } from '../../ui/modal'; import { getPaymentMethod } from '../payment-methods.mock'; import { AdyenPaymentMethodProps } from './AdyenV3PaymentMethod'; -import HostedWidgetPaymentMethod, { HostedWidgetPaymentMethodProps } from './HostedWidgetPaymentMethod'; +import HostedWidgetPaymentMethod, { + HostedWidgetPaymentMethodProps, +} from './HostedWidgetPaymentMethod'; import { default as PaymentMethodComponent, PaymentMethodProps } from './PaymentMethod'; describe('when using Adyen V3 payment', () => { @@ -34,23 +41,17 @@ describe('when using Adyen V3 payment', () => { localeContext = createLocaleContext(getStoreConfig()); method = { ...getPaymentMethod(), id: 'scheme', gateway: 'adyenv3', method: 'scheme' }; - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - jest.spyOn(checkoutService, 'deinitializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'deinitializePayment').mockResolvedValue(checkoutState); - jest.spyOn(checkoutService, 'initializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'initializePayment').mockResolvedValue(checkoutState); - PaymentMethodTest = props => ( - - - - + PaymentMethodTest = (props) => ( + + + + @@ -58,21 +59,24 @@ describe('when using Adyen V3 payment', () => { }); it('renders as hosted widget method', () => { - const container = mount(); - const component: ReactWrapper = container.find(HostedWidgetPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(HostedWidgetPaymentMethod); - expect(component.props()) - .toEqual(expect.objectContaining({ + expect(component.props()).toEqual( + expect.objectContaining({ containerId: 'adyen-scheme-component-field', deinitializePayment: expect.any(Function), initializePayment: expect.any(Function), method, - })); + }), + ); }); it('initializes method with required config', () => { - const container = mount(); - const component: ReactWrapper = container.find(HostedWidgetPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(HostedWidgetPaymentMethod); component.prop('initializePayment')({ methodId: method.id, @@ -81,8 +85,8 @@ describe('when using Adyen V3 payment', () => { expect(checkoutService.initializePayment).toHaveBeenCalled(); - expect(checkoutService.initializePayment) - .toHaveBeenCalledWith(expect.objectContaining({ + expect(checkoutService.initializePayment).toHaveBeenCalledWith( + expect.objectContaining({ adyenv3: { cardVerificationContainerId: undefined, containerId: 'adyen-scheme-component-field', @@ -102,7 +106,8 @@ describe('when using Adyen V3 payment', () => { }, gatewayId: method.gateway, methodId: method.id, - })); + }), + ); }); describe('#During payment', () => { @@ -114,31 +119,33 @@ describe('when using Adyen V3 payment', () => { method: getPaymentMethod(), onUnhandledError: jest.fn(), }; - const container = mount(); - const component: ReactWrapper = container.find(HostedWidgetPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(HostedWidgetPaymentMethod); component.prop('initializePayment')({ methodId: method.id, gatewayId: method.gateway, }); - const initializeOptions = (defaultAdyenProps.initializePayment as jest.Mock).mock.calls[0][0]; + const initializeOptions = (defaultAdyenProps.initializePayment as jest.Mock).mock + .calls[0][0]; act(() => { initializeOptions.adyenv3.additionalActionOptions.onBeforeLoad(true); }); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); act(() => { container.update(); }); - expect(container.find(Modal).prop('isOpen')) - .toEqual(true); + expect(container.find(Modal).prop('isOpen')).toBe(true); - expect(container.find('#adyen-scheme-additional-action-component-field')) - .toHaveLength(1); + expect(container.find('#adyen-scheme-additional-action-component-field')).toHaveLength( + 1, + ); }); it('Do not render 3DS modal if required by selected method', async () => { @@ -149,31 +156,33 @@ describe('when using Adyen V3 payment', () => { method: getPaymentMethod(), onUnhandledError: jest.fn(), }; - const container = mount(); - const component: ReactWrapper = container.find(HostedWidgetPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(HostedWidgetPaymentMethod); component.prop('initializePayment')({ methodId: method.id, gatewayId: method.gateway, }); - const initializeOptions = (defaultAdyenProps.initializePayment as jest.Mock).mock.calls[0][0]; + const initializeOptions = (defaultAdyenProps.initializePayment as jest.Mock).mock + .calls[0][0]; act(() => { initializeOptions.adyenv3.additionalActionOptions.onBeforeLoad(false); }); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); act(() => { container.update(); }); - expect(container.find(Modal).prop('isOpen')) - .toEqual(false); + expect(container.find(Modal).prop('isOpen')).toBe(false); - expect(container.find('#adyen-scheme-additional-action-component-field')) - .toHaveLength(1); + expect(container.find('#adyen-scheme-additional-action-component-field')).toHaveLength( + 1, + ); }); it('cancels 3DS modal flow if user chooses to close modal', async () => { @@ -185,21 +194,26 @@ describe('when using Adyen V3 payment', () => { method: getPaymentMethod(), onUnhandledError: jest.fn(), }; - const container = mount(); - const component: ReactWrapper = container.find(HostedWidgetPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(HostedWidgetPaymentMethod); component.prop('initializePayment')({ methodId: method.id, gatewayId: method.gateway, }); - const initializeOptions = (defaultAdyenProps.initializePayment as jest.Mock).mock.calls[0][0]; + const initializeOptions = (defaultAdyenProps.initializePayment as jest.Mock).mock + .calls[0][0]; act(() => { - initializeOptions.adyenv3.additionalActionOptions.onLoad(cancelAdditionalActionModalFlow, true); + initializeOptions.adyenv3.additionalActionOptions.onLoad( + cancelAdditionalActionModalFlow, + true, + ); }); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); act(() => { container.update(); diff --git a/packages/core/src/app/payment/paymentMethod/AdyenV3PaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/AdyenV3PaymentMethod.tsx index 54ad2da6ed..5f7a8f7ba9 100644 --- a/packages/core/src/app/payment/paymentMethod/AdyenV3PaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/AdyenV3PaymentMethod.tsx @@ -1,15 +1,24 @@ -import { AdyenV3CreditCardComponentOptions, AdyenV3ValidationState, CardInstrument } from '@bigcommerce/checkout-sdk'; +import { + AdyenV3CreditCardComponentOptions, + AdyenV3ValidationState, + CardInstrument, +} from '@bigcommerce/checkout-sdk'; import _ from 'lodash'; -import React, { useCallback, useRef, useState, FunctionComponent } from 'react'; +import React, { FunctionComponent, useCallback, useRef, useState } from 'react'; import { Omit } from 'utility-types'; import { TranslatedString } from '../../locale'; import { Modal } from '../../ui/modal'; import AdyenV3CardValidation from './AdyenV3CardValidation'; -import HostedWidgetPaymentMethod, { HostedWidgetPaymentMethodProps } from './HostedWidgetPaymentMethod'; +import HostedWidgetPaymentMethod, { + HostedWidgetPaymentMethodProps, +} from './HostedWidgetPaymentMethod'; -export type AdyenPaymentMethodProps = Omit; +export type AdyenPaymentMethodProps = Omit< + HostedWidgetPaymentMethodProps, + 'containerId' | 'hideContentWhenSignedOut' +>; export interface AdyenOptions { [key: string]: AdyenV3CreditCardComponentOptions; @@ -49,7 +58,7 @@ const AdyenV3PaymentMethod: FunctionComponent = ({ }, }; - const onBeforeLoad = useCallback((shopperInteraction: boolean) => { + const onBeforeLoad = useCallback((shopperInteraction: boolean) => { ref.current.shouldShowModal = shopperInteraction; if (ref.current.shouldShowModal) { @@ -77,7 +86,10 @@ const AdyenV3PaymentMethod: FunctionComponent = ({ } }, []); - const initializeAdyenPayment: HostedWidgetPaymentMethodProps['initializePayment'] = (options, selectedInstrument) => { + const initializeAdyenPayment: HostedWidgetPaymentMethodProps['initializePayment'] = ( + options, + selectedInstrument, + ) => { const selectedInstrumentId = selectedInstrument?.bigpayToken; return initializePayment({ @@ -86,7 +98,7 @@ const AdyenV3PaymentMethod: FunctionComponent = ({ cardVerificationContainerId: selectedInstrumentId && cardVerificationContainerId, containerId, hasVaultedInstruments: !!selectedInstrumentId, - ...(adyenOptions[component] ? {options: adyenOptions[component]} : {}), + ...(adyenOptions[component] ? { options: adyenOptions[component] } : {}), additionalActionOptions: { widgetSize: '05', containerId: additionalActionContainerId, @@ -95,63 +107,70 @@ const AdyenV3PaymentMethod: FunctionComponent = ({ onLoad, }, shouldShowNumberField: ref.current.shouldShowNumberField, - validateCardFields: (state: AdyenV3ValidationState) => { setCardValidationState(state); }, + validateCardFields: (state: AdyenV3ValidationState) => { + setCardValidationState(state); + }, }, }); }; - const validateInstrument = (shouldShowNumberField: boolean, selectedInstrument: CardInstrument) => { - + const validateInstrument = ( + shouldShowNumberField: boolean, + selectedInstrument: CardInstrument, + ) => { ref.current.shouldShowNumberField = shouldShowNumberField; - return ; + return ( + + ); }; const isAccountInstrument = () => { switch (method.method) { - case 'directEbanking': - case 'giropay': - case 'ideal': - case 'sepadirectdebit': - return true; - default: - return false; + case 'directEbanking': + case 'giropay': + case 'ideal': + case 'sepadirectdebit': + return true; + + default: + return false; } }; - return <> - - - } - isOpen={ showAdditionalActionContent } - onRequestClose={ cancelAdditionalActionModalFlow } - shouldShowCloseButton={ true } - > -
    - - { !showAdditionalActionContent && -
    } - ; + return ( + <> + + + } + isOpen={showAdditionalActionContent} + onRequestClose={cancelAdditionalActionModalFlow} + shouldShowCloseButton={true} + > +
    + + {!showAdditionalActionContent && ( +
    + )} + + ); }; export default AdyenV3PaymentMethod; diff --git a/packages/core/src/app/payment/paymentMethod/AffirmPaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/AffirmPaymentMethod.spec.tsx index 45f716e20e..bcd70fcb91 100644 --- a/packages/core/src/app/payment/paymentMethod/AffirmPaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/AffirmPaymentMethod.spec.tsx @@ -19,7 +19,8 @@ describe('When using Affirm Payment Method', () => { }; it('Shopper is able to see Affirm Payment Method', () => { - const component = mount(); + const component = mount(); + expect(component.find(HostedWidgetPaymentMethod)).toBeTruthy(); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/AffirmPaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/AffirmPaymentMethod.tsx index 11a0a43d39..cb5e969268 100644 --- a/packages/core/src/app/payment/paymentMethod/AffirmPaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/AffirmPaymentMethod.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, FunctionComponent } from 'react'; +import React, { FunctionComponent, useMemo } from 'react'; import { Omit } from 'utility-types'; import { TranslatedString } from '../../locale'; @@ -7,13 +7,10 @@ import HostedPaymentMethod, { HostedPaymentMethodProps } from './HostedPaymentMe export type AffirmPaymentMethodProps = Omit; -const AffirmPaymentMethod: FunctionComponent = props => { +const AffirmPaymentMethod: FunctionComponent = (props) => { const description = useMemo(() => , []); - return ; + return ; }; export default AffirmPaymentMethod; diff --git a/packages/core/src/app/payment/paymentMethod/AmazonPayV2PaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/AmazonPayV2PaymentMethod.spec.tsx index 66f38745f3..901b9ec617 100644 --- a/packages/core/src/app/payment/paymentMethod/AmazonPayV2PaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/AmazonPayV2PaymentMethod.spec.tsx @@ -1,4 +1,9 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService, PaymentMethod } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, + PaymentMethod, +} from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import { Formik } from 'formik'; import { noop } from 'lodash'; @@ -9,7 +14,9 @@ import { getStoreConfig } from '../../config/config.mock'; import { createLocaleContext, LocaleContext, LocaleContextType } from '../../locale'; import { getPaymentMethod } from '../payment-methods.mock'; -import HostedWidgetPaymentMethod, { HostedWidgetPaymentMethodProps } from './HostedWidgetPaymentMethod'; +import HostedWidgetPaymentMethod, { + HostedWidgetPaymentMethodProps, +} from './HostedWidgetPaymentMethod'; import { default as PaymentMethodComponent, PaymentMethodProps } from './PaymentMethod'; import PaymentMethodId from './PaymentMethodId'; @@ -30,30 +37,26 @@ describe('when using AmazonPay payment', () => { checkoutService = createCheckoutService(); checkoutState = checkoutService.getState(); localeContext = createLocaleContext(getStoreConfig()); - method = { ...getPaymentMethod(), + method = { + ...getPaymentMethod(), id: PaymentMethodId.AmazonPay, initializationData: { paymentDescriptor: 'Hey Amazon', paymentToken: 'abcdefg', - }}; + }, + }; - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - jest.spyOn(checkoutService, 'deinitializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'deinitializePayment').mockResolvedValue(checkoutState); - jest.spyOn(checkoutService, 'initializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'initializePayment').mockResolvedValue(checkoutState); - PaymentMethodTest = props => ( - - - - + PaymentMethodTest = (props) => ( + + + + @@ -61,11 +64,12 @@ describe('when using AmazonPay payment', () => { }); it('renders as hosted widget method', () => { - const container = mount(); - const component: ReactWrapper = container.find(HostedWidgetPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(HostedWidgetPaymentMethod); - expect(component.props()) - .toEqual(expect.objectContaining({ + expect(component.props()).toEqual( + expect.objectContaining({ buttonId: 'editButtonId', containerId: 'paymentWidget', deinitializeCustomer: undefined, @@ -78,25 +82,28 @@ describe('when using AmazonPay payment', () => { paymentDescriptor: 'Hey Amazon', shouldShowDescriptor: true, shouldShowEditButton: true, - })); + }), + ); }); it('initializes method with required config', () => { - const container = mount(); - const component: ReactWrapper = container.find(HostedWidgetPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(HostedWidgetPaymentMethod); component.prop('initializePayment')({ methodId: method.id, gatewayId: method.gateway, }); - expect(checkoutService.initializePayment) - .toHaveBeenCalledWith(expect.objectContaining({ + expect(checkoutService.initializePayment).toHaveBeenCalledWith( + expect.objectContaining({ methodId: method.id, gatewayId: method.gateway, [method.id]: { editButtonId: 'editButtonId', }, - })); + }), + ); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/AmazonPayV2PaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/AmazonPayV2PaymentMethod.tsx index ea5b7f1374..07c6e69073 100644 --- a/packages/core/src/app/payment/paymentMethod/AmazonPayV2PaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/AmazonPayV2PaymentMethod.tsx @@ -1,42 +1,65 @@ import { PaymentInitializeOptions } from '@bigcommerce/checkout-sdk'; -import React, { useCallback, FunctionComponent } from 'react'; +import React, { FunctionComponent, useCallback } from 'react'; import { Omit } from 'utility-types'; -import HostedWidgetPaymentMethod , { HostedWidgetPaymentMethodProps } from './HostedWidgetPaymentMethod'; +import HostedWidgetPaymentMethod, { + HostedWidgetPaymentMethodProps, +} from './HostedWidgetPaymentMethod'; -export type AmazonPayV2PaymentMethodProps = Omit; +export type AmazonPayV2PaymentMethodProps = Omit< + HostedWidgetPaymentMethodProps, + | 'buttonId' + | 'containerId' + | 'deinitializeCustomer' + | 'hideWidget' + | 'initializeCustomer' + | 'isSignInRequired' + | 'onSignOut' + | 'paymentDescriptor' + | 'shouldShow' + | 'shouldShowDescriptor' + | 'shouldShowEditButton' +>; const AmazonPayV2PaymentMethod: FunctionComponent = ({ initializePayment, method, - method: { initializationData: { paymentDescriptor, paymentToken } }, + method: { + initializationData: { paymentDescriptor, paymentToken }, + }, ...rest }) => { - const initializeAmazonPayV2Payment = useCallback((options: PaymentInitializeOptions) => initializePayment({ - ...options, - amazonpay: { - editButtonId: 'editButtonId', - }, - }), [initializePayment]); + const initializeAmazonPayV2Payment = useCallback( + (options: PaymentInitializeOptions) => + initializePayment({ + ...options, + amazonpay: { + editButtonId: 'editButtonId', + }, + }), + [initializePayment], + ); const reload = useCallback(() => window.location.reload(), []); - return ; + return ( + + ); }; export default AmazonPayV2PaymentMethod; diff --git a/packages/core/src/app/payment/paymentMethod/AmazonPaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/AmazonPaymentMethod.spec.tsx index a3ef3fb95e..e28bd2877a 100644 --- a/packages/core/src/app/payment/paymentMethod/AmazonPaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/AmazonPaymentMethod.spec.tsx @@ -40,37 +40,37 @@ describe('When using AmazonPaymentMethod', () => { onUnhandledError: jest.fn(), }; - const TestComponent: FunctionComponent> = props => - - - ; + const TestComponent: FunctionComponent> = (props) => ( + + + + ); component = mount(); }); it('Shopper should be able to see Amazon Payment Method', () => { - expect(component.find(HostedWidgetPaymentMethod).exists).toBeTruthy(); }); it('Shopper should be able to SignInAmazon', () => { const onClick = jest.fn(); - jest.spyOn(document, 'querySelector') - .mockImplementation((selector: string) => { - const element = document.createElement('div'); - element.id = selector; - element.addEventListener('click', onClick); - return element; - }); + jest.spyOn(document, 'querySelector').mockImplementation((selector: string) => { + const element = document.createElement('div'); + + element.id = selector; + element.addEventListener('click', onClick); + + return element; + }); const hostedWidget = component.find(HostedWidgetPaymentMethod); const { signInCustomer = noop } = hostedWidget.props(); + signInCustomer(); + expect(onClick).toHaveBeenCalled(); }); @@ -82,7 +82,7 @@ describe('When using AmazonPaymentMethod', () => { methodId: defaultProps.method.id, }); - expect(defaultProps.initializeCustomer).toBeCalledWith({ + expect(defaultProps.initializeCustomer).toHaveBeenCalledWith({ methodId: 'amazon', amazon: { container: 'paymentWidget', @@ -98,6 +98,7 @@ describe('When using AmazonPaymentMethod', () => { initializePayment({ methodId: defaultProps.method.id, }); + const { onError = noop } = initializePaymentOptions.amazon || {}; onError({ message: 'An error' }); @@ -110,7 +111,7 @@ describe('When using AmazonPaymentMethod', () => { expect.objectContaining({ id: 'amazon', }), - true + true, ); }); @@ -121,6 +122,7 @@ describe('When using AmazonPaymentMethod', () => { initializePayment({ methodId: defaultProps.method.id, }); + const { onPaymentSelect = noop } = initializePaymentOptions.amazon || {}; onPaymentSelect(); @@ -129,7 +131,7 @@ describe('When using AmazonPaymentMethod', () => { expect.objectContaining({ id: 'amazon', }), - false + false, ); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/AmazonPaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/AmazonPaymentMethod.tsx index df390e13c9..c259e25f61 100644 --- a/packages/core/src/app/payment/paymentMethod/AmazonPaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/AmazonPaymentMethod.tsx @@ -1,12 +1,22 @@ -import { CheckoutSelectors, CustomerInitializeOptions, PaymentInitializeOptions } from '@bigcommerce/checkout-sdk'; -import React, { useCallback, useContext, FunctionComponent } from 'react'; +import { + CheckoutSelectors, + CustomerInitializeOptions, + PaymentInitializeOptions, +} from '@bigcommerce/checkout-sdk'; +import React, { FunctionComponent, useCallback, useContext } from 'react'; import { Omit } from 'utility-types'; import PaymentContext from '../PaymentContext'; -import HostedWidgetPaymentMethod, { HostedWidgetPaymentMethodProps } from './HostedWidgetPaymentMethod'; +import HostedWidgetPaymentMethod, { + HostedWidgetPaymentMethodProps, +} from './HostedWidgetPaymentMethod'; -export interface AmazonPaymentMethodProps extends Omit { +export interface AmazonPaymentMethodProps + extends Omit< + HostedWidgetPaymentMethodProps, + 'containerId' | 'hideContentWhenSignedOut' | 'isSignInRequired' | 'signInCustomer' + > { initializeCustomer(options: CustomerInitializeOptions): Promise; } @@ -25,43 +35,54 @@ const AmazonPaymentMethod: FunctionComponent = ({ ...rest }) => { const paymentContext = useContext(PaymentContext); - const initializeAmazonCustomer = useCallback((options: CustomerInitializeOptions) => initializeCustomer({ - ...options, - amazon: { - container: 'paymentWidget', - onError: onUnhandledError, - }, - }), [initializeCustomer, onUnhandledError]); + const initializeAmazonCustomer = useCallback( + (options: CustomerInitializeOptions) => + initializeCustomer({ + ...options, + amazon: { + container: 'paymentWidget', + onError: onUnhandledError, + }, + }), + [initializeCustomer, onUnhandledError], + ); - const initializeAmazonPayment = useCallback((options: PaymentInitializeOptions) => initializePayment({ - ...options, - amazon: { - container: 'paymentWidget', - onError: (error: Error) => { - if (onUnhandledError) { - onUnhandledError(error); - } - if (paymentContext) { - paymentContext.disableSubmit(rest.method, true); - } - }, - onPaymentSelect: () => { - if (paymentContext) { - paymentContext.disableSubmit(rest.method, false); - } - }, - }, - }), [initializePayment, onUnhandledError, paymentContext, rest.method]); + const initializeAmazonPayment = useCallback( + (options: PaymentInitializeOptions) => + initializePayment({ + ...options, + amazon: { + container: 'paymentWidget', + onError: (error: Error) => { + if (onUnhandledError) { + onUnhandledError(error); + } - return ; + if (paymentContext) { + paymentContext.disableSubmit(rest.method, true); + } + }, + onPaymentSelect: () => { + if (paymentContext) { + paymentContext.disableSubmit(rest.method, false); + } + }, + }, + }), + [initializePayment, onUnhandledError, paymentContext, rest.method], + ); + + return ( + + ); }; export default AmazonPaymentMethod; diff --git a/packages/core/src/app/payment/paymentMethod/ApplePayPaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/ApplePayPaymentMethod.spec.tsx index a3dda0dd1d..6da235b975 100644 --- a/packages/core/src/app/payment/paymentMethod/ApplePayPaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/ApplePayPaymentMethod.spec.tsx @@ -1,4 +1,8 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, +} from '@bigcommerce/checkout-sdk'; import { mount } from 'enzyme'; import { Formik } from 'formik'; import { noop } from 'lodash'; @@ -32,9 +36,7 @@ describe('ApplePayPaymentMethod', () => { ...getPaymentMethod(), id: PaymentMethodId.ApplePay, initializationData: { - merchantCapabilities: [ - 'supports3DS', - ], + merchantCapabilities: ['supports3DS'], }, }, }; @@ -49,24 +51,18 @@ describe('ApplePayPaymentMethod', () => { hidePaymentSubmitButton: jest.fn(), }; - jest.spyOn(checkoutState.data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(checkoutState.data, 'getCart').mockReturnValue(getCart()); - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - jest.spyOn(checkoutState.data, 'getCustomer') - .mockReturnValue(getCustomer()); + jest.spyOn(checkoutState.data, 'getCustomer').mockReturnValue(getCustomer()); - PaymentMethodTest = props => ( - - - - - + PaymentMethodTest = (props) => ( + + + + + @@ -77,19 +73,20 @@ describe('ApplePayPaymentMethod', () => { it('renders as hosted method', () => { const container = mount(); - expect(container.find(HostedPaymentMethod).length).toEqual(1); + expect(container.find(HostedPaymentMethod)).toHaveLength(1); }); it('initializes method with required config', () => { mount(); - expect(defaultProps.initializePayment) - .toHaveBeenCalledWith(expect.objectContaining({ + expect(defaultProps.initializePayment).toHaveBeenCalledWith( + expect.objectContaining({ methodId: defaultProps.method.id, [defaultProps.method.id]: { shippingLabel: 'Shipping', subtotalLabel: 'Subtotal', }, - })); + }), + ); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/ApplePayPaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/ApplePayPaymentMethod.tsx index 677b4fbc16..c554554946 100644 --- a/packages/core/src/app/payment/paymentMethod/ApplePayPaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/ApplePayPaymentMethod.tsx @@ -1,5 +1,5 @@ import { PaymentInitializeOptions } from '@bigcommerce/checkout-sdk'; -import React, { useCallback, useContext, FunctionComponent } from 'react'; +import React, { FunctionComponent, useCallback, useContext } from 'react'; import { LocaleContext } from '../../locale'; @@ -12,19 +12,19 @@ const ApplePayPaymentMethod: FunctionComponent = ({ }) => { const localeContext = useContext(LocaleContext); - const initializeApplePay = useCallback((options: PaymentInitializeOptions) => initializePayment({ - ...options, - applepay: { - shippingLabel: localeContext?.language.translate('cart.shipping_text'), - subtotalLabel: localeContext?.language.translate('cart.subtotal_text'), - }, - }), [initializePayment, localeContext]); + const initializeApplePay = useCallback( + (options: PaymentInitializeOptions) => + initializePayment({ + ...options, + applepay: { + shippingLabel: localeContext?.language.translate('cart.shipping_text'), + subtotalLabel: localeContext?.language.translate('cart.subtotal_text'), + }, + }), + [initializePayment, localeContext], + ); - return ; + return ; }; export default ApplePayPaymentMethod; diff --git a/packages/core/src/app/payment/paymentMethod/BarclaycardPaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/BarclaycardPaymentMethod.spec.tsx index 9105219d02..30d97f36a5 100644 --- a/packages/core/src/app/payment/paymentMethod/BarclaycardPaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/BarclaycardPaymentMethod.spec.tsx @@ -1,8 +1,12 @@ import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; -import BarclaycardPaymentMethod, { BarclaycardPaymentMethodProps } from './BarclaycardPaymentMethod'; -import HostedWidgetPaymentMethod, { HostedWidgetPaymentMethodProps } from './HostedWidgetPaymentMethod'; +import BarclaycardPaymentMethod, { + BarclaycardPaymentMethodProps, +} from './BarclaycardPaymentMethod'; +import HostedWidgetPaymentMethod, { + HostedWidgetPaymentMethodProps, +} from './HostedWidgetPaymentMethod'; describe('when using Barclaycard payment', () => { const defaultProps: BarclaycardPaymentMethodProps = { @@ -19,8 +23,9 @@ describe('when using Barclaycard payment', () => { }; it('should mount Barclaycard', () => { - const container = mount(); - const component: ReactWrapper = container.find(HostedWidgetPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(HostedWidgetPaymentMethod); expect(container.find(HostedWidgetPaymentMethod)).toBeTruthy(); expect(component.prop('containerId')).toBe('barclaycard-container'); diff --git a/packages/core/src/app/payment/paymentMethod/BarclaycardPaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/BarclaycardPaymentMethod.tsx index aeddeeaedb..de0a83c37b 100644 --- a/packages/core/src/app/payment/paymentMethod/BarclaycardPaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/BarclaycardPaymentMethod.tsx @@ -1,19 +1,18 @@ import React, { FunctionComponent } from 'react'; -import HostedWidgetPaymentMethod, { HostedWidgetPaymentMethodProps } from './HostedWidgetPaymentMethod'; +import HostedWidgetPaymentMethod, { + HostedWidgetPaymentMethodProps, +} from './HostedWidgetPaymentMethod'; -export type BarclaycardPaymentMethodProps = Omit< HostedWidgetPaymentMethodProps, 'containerId'>; - -const BarclaycardPaymentMethod: FunctionComponent = props => { +export type BarclaycardPaymentMethodProps = Omit; +const BarclaycardPaymentMethod: FunctionComponent = (props) => { const { method } = props; const containerId = `${method.id}-container`; - return ; + return ( + + ); }; export default BarclaycardPaymentMethod; diff --git a/packages/core/src/app/payment/paymentMethod/BlueSnapV2PaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/BlueSnapV2PaymentMethod.spec.tsx index 40b9c540fe..17dc673226 100644 --- a/packages/core/src/app/payment/paymentMethod/BlueSnapV2PaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/BlueSnapV2PaymentMethod.spec.tsx @@ -1,4 +1,8 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, +} from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import { Formik } from 'formik'; import { noop } from 'lodash'; @@ -48,24 +52,18 @@ describe('when using BlueSnapV2 payment', () => { hidePaymentSubmitButton: jest.fn(), }; - jest.spyOn(checkoutState.data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(checkoutState.data, 'getCart').mockReturnValue(getCart()); - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - jest.spyOn(checkoutState.data, 'getCustomer') - .mockReturnValue(getCustomer()); + jest.spyOn(checkoutState.data, 'getCustomer').mockReturnValue(getCustomer()); - BlueSnapV2PaymentMethodTest = props => ( - - - - - + BlueSnapV2PaymentMethodTest = (props) => ( + + + + + @@ -76,15 +74,14 @@ describe('when using BlueSnapV2 payment', () => { it('renders as hosted payment method', () => { const container = mount(); - expect(container.find(HostedPaymentMethod).length) - .toEqual(1); + expect(container.find(HostedPaymentMethod)).toHaveLength(1); }); it('initializes method with required config', () => { mount(); - expect(defaultProps.initializePayment) - .toHaveBeenCalledWith(expect.objectContaining({ + expect(defaultProps.initializePayment).toHaveBeenCalledWith( + expect.objectContaining({ methodId: 'mastercard', gatewayId: 'bluesnapv2', bluesnapv2: { @@ -95,7 +92,8 @@ describe('when using BlueSnapV2 payment', () => { width: expect.any(String), }, }, - })); + }), + ); }); it('renders modal that hosts bluesnap payment page', async () => { @@ -103,20 +101,16 @@ describe('when using BlueSnapV2 payment', () => { const initializeOptions = (defaultProps.initializePayment as jest.Mock).mock.calls[0][0]; act(() => { - initializeOptions.bluesnapv2.onLoad( - document.createElement('iframe'), - jest.fn() - ); + initializeOptions.bluesnapv2.onLoad(document.createElement('iframe'), jest.fn()); }); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); act(() => { component.update(); }); - expect(component.find(Modal).prop('isOpen')) - .toEqual(true); + expect(component.find(Modal).prop('isOpen')).toBe(true); }); it('renders modal but does not append bluesnap payment page because is empty', async () => { @@ -124,13 +118,10 @@ describe('when using BlueSnapV2 payment', () => { const initializeOptions = (defaultProps.initializePayment as jest.Mock).mock.calls[0][0]; act(() => { - initializeOptions.bluesnapv2.onLoad( - undefined, - jest.fn() - ); + initializeOptions.bluesnapv2.onLoad(undefined, jest.fn()); }); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); act(() => { component.update(); @@ -138,11 +129,9 @@ describe('when using BlueSnapV2 payment', () => { component.find(Modal).prop('onAfterOpen')!(); }); - expect(component.find(Modal).prop('isOpen')) - .toEqual(false); + expect(component.find(Modal).prop('isOpen')).toBe(false); - expect(component.find(Modal).render().find('iframe')) - .toHaveLength(0); + expect(component.find(Modal).render().find('iframe')).toHaveLength(0); }); it('cancels payment flow if user chooses to close modal', async () => { @@ -153,11 +142,11 @@ describe('when using BlueSnapV2 payment', () => { act(() => { initializeOptions.bluesnapv2.onLoad( document.createElement('iframe'), - cancelBlueSnapV2Payment + cancelBlueSnapV2Payment, ); }); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); act(() => { component.update(); @@ -172,7 +161,6 @@ describe('when using BlueSnapV2 payment', () => { modal.prop('onRequestClose')!(new MouseEvent('click') as any); }); - expect(cancelBlueSnapV2Payment) - .toHaveBeenCalledTimes(1); + expect(cancelBlueSnapV2Payment).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/BlueSnapV2PaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/BlueSnapV2PaymentMethod.tsx index 225be0a664..49414e8d46 100644 --- a/packages/core/src/app/payment/paymentMethod/BlueSnapV2PaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/BlueSnapV2PaymentMethod.tsx @@ -1,7 +1,14 @@ import { PaymentInitializeOptions } from '@bigcommerce/checkout-sdk'; -import React, { createRef, useCallback, useRef, useState, FunctionComponent, RefObject } from 'react'; +import React, { + createRef, + FunctionComponent, + RefObject, + useCallback, + useRef, + useState, +} from 'react'; -import { LoadingOverlay } from "../../ui/loading"; +import { LoadingOverlay } from '../../ui/loading'; import { Modal } from '../../ui/modal'; import HostedPaymentMethod, { HostedPaymentMethodProps } from './HostedPaymentMethod'; @@ -32,50 +39,48 @@ const BlueSnapV2PaymentMethod: FunctionComponent = } }, []); - const initializeBlueSnapV2Payment = useCallback((options: PaymentInitializeOptions) => { - return initializePayment({ - ...options, - bluesnapv2: { - onLoad(content: HTMLIFrameElement, cancel: () => void) { - setPaymentPageContent(content); - setisLoadingIframe(true); - ref.current.cancelBlueSnapV2Payment = cancel; + const initializeBlueSnapV2Payment = useCallback( + (options: PaymentInitializeOptions) => { + return initializePayment({ + ...options, + bluesnapv2: { + onLoad(content: HTMLIFrameElement, cancel: () => void) { + setPaymentPageContent(content); + setisLoadingIframe(true); + ref.current.cancelBlueSnapV2Payment = cancel; + }, + style: { + border: '1px solid lightgray', + height: '60vh', + width: '100%', + }, }, - style: { - border: '1px solid lightgray', - height: '60vh', - width: '100%', - }, - }, - }); - }, [initializePayment]); + }); + }, + [initializePayment], + ); const appendPaymentPageContent = useCallback(() => { if (ref.current.paymentPageContentRef.current && paymentPageContent) { ref.current.paymentPageContentRef.current.appendChild(paymentPageContent); - paymentPageContent.addEventListener('load', - () => { - setisLoadingIframe(false); - } - ); + paymentPageContent.addEventListener('load', () => { + setisLoadingIframe(false); + }); } }, [paymentPageContent]); return ( <> - + - -
    + +
    diff --git a/packages/core/src/app/payment/paymentMethod/BoltClientPaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/BoltClientPaymentMethod.spec.tsx index f2410e0793..69c12312f8 100644 --- a/packages/core/src/app/payment/paymentMethod/BoltClientPaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/BoltClientPaymentMethod.spec.tsx @@ -1,4 +1,8 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, +} from '@bigcommerce/checkout-sdk'; import { mount } from 'enzyme'; import { Formik } from 'formik'; import { noop } from 'lodash'; @@ -48,24 +52,18 @@ describe('BoltClientPaymentMethod', () => { hidePaymentSubmitButton: jest.fn(), }; - jest.spyOn(checkoutState.data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(checkoutState.data, 'getCart').mockReturnValue(getCart()); - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - jest.spyOn(checkoutState.data, 'getCustomer') - .mockReturnValue(getCustomer()); + jest.spyOn(checkoutState.data, 'getCustomer').mockReturnValue(getCustomer()); - PaymentMethodTest = props => ( - - - - - + PaymentMethodTest = (props) => ( + + + + + @@ -76,18 +74,19 @@ describe('BoltClientPaymentMethod', () => { it('renders as hosted method', () => { const container = mount(); - expect(container.find(HostedPaymentMethod).length).toEqual(1); + expect(container.find(HostedPaymentMethod)).toHaveLength(1); }); it('initializes method with required config', () => { mount(); - expect(defaultProps.initializePayment) - .toHaveBeenCalledWith(expect.objectContaining({ + expect(defaultProps.initializePayment).toHaveBeenCalledWith( + expect.objectContaining({ methodId: defaultProps.method.id, [defaultProps.method.id]: { useBigCommerceCheckout: true, }, - })); + }), + ); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/BoltClientPaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/BoltClientPaymentMethod.tsx index 2af510582c..f707c95278 100644 --- a/packages/core/src/app/payment/paymentMethod/BoltClientPaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/BoltClientPaymentMethod.tsx @@ -1,5 +1,5 @@ import { PaymentInitializeOptions } from '@bigcommerce/checkout-sdk'; -import React, { useCallback, FunctionComponent } from 'react'; +import React, { FunctionComponent, useCallback } from 'react'; import HostedPaymentMethod, { HostedPaymentMethodProps } from './HostedPaymentMethod'; import { HostedWidgetPaymentMethodProps } from './HostedWidgetPaymentMethod'; @@ -9,19 +9,20 @@ const BoltClientPaymentMethod: FunctionComponent = ({ method, ...rest }) => { - const initializeBoltPayment: HostedWidgetPaymentMethodProps['initializePayment'] = useCallback((options: PaymentInitializeOptions) => initializePayment({ - ...options, - bolt: { - useBigCommerceCheckout: true, - }, - } - ), [initializePayment]); + const initializeBoltPayment: HostedWidgetPaymentMethodProps['initializePayment'] = useCallback( + (options: PaymentInitializeOptions) => + initializePayment({ + ...options, + bolt: { + useBigCommerceCheckout: true, + }, + }), + [initializePayment], + ); - return ; + return ( + + ); }; export default BoltClientPaymentMethod; diff --git a/packages/core/src/app/payment/paymentMethod/BoltCustomForm.spec.tsx b/packages/core/src/app/payment/paymentMethod/BoltCustomForm.spec.tsx index 3d8261c48f..d531eec0cc 100644 --- a/packages/core/src/app/payment/paymentMethod/BoltCustomForm.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/BoltCustomForm.spec.tsx @@ -5,7 +5,6 @@ import React, { FunctionComponent } from 'react'; import BoltCustomForm, { BoltCustomFormProps } from './BoltCustomForm'; -/* eslint-disable react/jsx-no-bind */ describe('BoltCustomForm', () => { let defaultProps: BoltCustomFormProps; let BoltCustomFormTest: FunctionComponent; @@ -16,58 +15,56 @@ describe('BoltCustomForm', () => { showCreateAccountCheckbox: false, }; - BoltCustomFormTest = props => ( - - ); + BoltCustomFormTest = (props) => ; }); it('renders bolt embedded field', () => { const container = mount( } - /> + initialValues={{ shouldCreateAccount: true }} + onSubmit={noop} + render={() => } + />, ); - expect(container.find('[id="boltEmbeddedOneClick"]').exists()).toEqual(true); + expect(container.find('[id="boltEmbeddedOneClick"]').exists()).toBe(true); }); it('renders bolt embedded field and shows create account checkbox', () => { const container = mount( } - /> + initialValues={{ shouldCreateAccount: true }} + onSubmit={noop} + render={() => } + />, ); - expect(container.find('[id="boltEmbeddedOneClick"]').exists()).toEqual(true); - expect(container.find('[id="shouldCreateAccount"]').exists()).toEqual(true); + expect(container.find('[id="boltEmbeddedOneClick"]').exists()).toBe(true); + expect(container.find('[id="shouldCreateAccount"]').exists()).toBe(true); }); it('renders bolt embedded field without showing create account checkbox', () => { const container = mount( } - /> + initialValues={{ shouldCreateAccount: false }} + onSubmit={noop} + render={() => } + />, ); - expect(container.find('[id="boltEmbeddedOneClick"]').exists()).toEqual(true); - expect(container.find('[id="shouldCreateAccount"]').exists()).toEqual(false); + expect(container.find('[id="boltEmbeddedOneClick"]').exists()).toBe(true); + expect(container.find('[id="shouldCreateAccount"]').exists()).toBe(false); }); it('renders bolt embedded field with checked account creation checkbox by default', () => { const container = mount( } - /> + initialValues={{ shouldCreateAccount: true }} + onSubmit={noop} + render={() => } + />, ); - expect(container.find('[id="shouldCreateAccount"]').hostNodes().props().checked).toEqual(true); + expect(container.find('[id="shouldCreateAccount"]').hostNodes().props().checked).toBe(true); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/BoltCustomForm.tsx b/packages/core/src/app/payment/paymentMethod/BoltCustomForm.tsx index 8f57c61f8f..17a5b9248a 100644 --- a/packages/core/src/app/payment/paymentMethod/BoltCustomForm.tsx +++ b/packages/core/src/app/payment/paymentMethod/BoltCustomForm.tsx @@ -23,36 +23,36 @@ const BoltCreateAccountCheckbox: FunctionComponent = () => { const labelContent = ( <>
      - { - benefitsList.map(({ id }, key) => ( -
    • - -
    • - )) - } + {benefitsList.map(({ id }, key) => ( +
    • + +
    • + ))}
    ); - return ; + return ( + + ); }; -const BoltCustomForm: FunctionComponent = ({ containerId, showCreateAccountCheckbox }) => { +const BoltCustomForm: FunctionComponent = ({ + containerId, + showCreateAccountCheckbox, +}) => { return (
    -
    - { showCreateAccountCheckbox ? : null } +
    + {showCreateAccountCheckbox ? : null}
    ); }; diff --git a/packages/core/src/app/payment/paymentMethod/BoltEmbeddedPaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/BoltEmbeddedPaymentMethod.spec.tsx index 2a8e8f35d8..ff880546a6 100644 --- a/packages/core/src/app/payment/paymentMethod/BoltEmbeddedPaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/BoltEmbeddedPaymentMethod.spec.tsx @@ -1,4 +1,9 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService, PaymentMethod } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, + PaymentMethod, +} from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import { Formik } from 'formik'; import { noop } from 'lodash'; @@ -9,7 +14,9 @@ import { getStoreConfig } from '../../config/config.mock'; import { createLocaleContext, LocaleContext, LocaleContextType } from '../../locale'; import { getPaymentMethod } from '../payment-methods.mock'; -import HostedWidgetPaymentMethod, { HostedWidgetPaymentMethodProps } from './HostedWidgetPaymentMethod'; +import HostedWidgetPaymentMethod, { + HostedWidgetPaymentMethodProps, +} from './HostedWidgetPaymentMethod'; import { default as PaymentMethodComponent, PaymentMethodProps } from './PaymentMethod'; describe('BoltEmbeddedPaymentMethod', () => { @@ -39,23 +46,17 @@ describe('BoltEmbeddedPaymentMethod', () => { checkoutState = checkoutService.getState(); localeContext = createLocaleContext(getStoreConfig()); - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - jest.spyOn(checkoutService, 'deinitializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'deinitializePayment').mockResolvedValue(checkoutState); - jest.spyOn(checkoutService, 'initializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'initializePayment').mockResolvedValue(checkoutState); - PaymentMethodTest = props => ( - - - - + PaymentMethodTest = (props) => ( + + + + @@ -63,29 +64,32 @@ describe('BoltEmbeddedPaymentMethod', () => { }); it('renders as hosted widget method', () => { - const container = mount(); - const component: ReactWrapper = container.find(HostedWidgetPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(HostedWidgetPaymentMethod); - expect(component.props()) - .toEqual(expect.objectContaining({ + expect(component.props()).toEqual( + expect.objectContaining({ containerId: 'boltEmbeddedOneClick', deinitializePayment: expect.any(Function), initializePayment: expect.any(Function), method, - })); + }), + ); }); it('initializes method with required config', () => { - const container = mount(); - const component: ReactWrapper = container.find(HostedWidgetPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(HostedWidgetPaymentMethod); component.prop('initializePayment')({ methodId: method.id, gatewayId: method.gateway, }); - expect(checkoutService.initializePayment) - .toHaveBeenCalledWith(expect.objectContaining({ + expect(checkoutService.initializePayment).toHaveBeenCalledWith( + expect.objectContaining({ methodId: method.id, gatewayId: method.gateway, bolt: { @@ -93,6 +97,7 @@ describe('BoltEmbeddedPaymentMethod', () => { useBigCommerceCheckout: true, onPaymentSelect: expect.any(Function), }, - })); + }), + ); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/BoltEmbeddedPaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/BoltEmbeddedPaymentMethod.tsx index d9c05bab30..7e92bdb6fa 100644 --- a/packages/core/src/app/payment/paymentMethod/BoltEmbeddedPaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/BoltEmbeddedPaymentMethod.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState, FunctionComponent } from 'react'; +import React, { FunctionComponent, useCallback, useState } from 'react'; import BoltCustomForm from './BoltCustomForm'; import { HostedPaymentMethodProps } from './HostedPaymentMethod'; @@ -10,38 +10,46 @@ const BoltEmbeddedPaymentMethod: FunctionComponent = ( deinitializePayment, method, }) => { - const [ showCreateAccountCheckbox, setShowCreateAccountCheckbox ] = useState(false); + const [showCreateAccountCheckbox, setShowCreateAccountCheckbox] = useState(false); const boltEmbeddedContainerId = 'bolt-embedded'; - const initializeBoltPayment = useCallback(options => initializePayment({ - ...options, - bolt: { - containerId: boltEmbeddedContainerId, - useBigCommerceCheckout: true, - onPaymentSelect: (hasBoltAccount: boolean) => { - setShowCreateAccountCheckbox(!hasBoltAccount); + const initializeBoltPayment = useCallback( + (options) => + initializePayment({ + ...options, + bolt: { + containerId: boltEmbeddedContainerId, + useBigCommerceCheckout: true, + onPaymentSelect: (hasBoltAccount: boolean) => { + setShowCreateAccountCheckbox(!hasBoltAccount); + }, }, - }, - } - ), [initializePayment, boltEmbeddedContainerId]); + }), + [initializePayment, boltEmbeddedContainerId], + ); - const renderCustomPaymentForm = useCallback(() => ( - - ), [ boltEmbeddedContainerId, showCreateAccountCheckbox ]); + const renderCustomPaymentForm = useCallback( + () => ( + + ), + [boltEmbeddedContainerId, showCreateAccountCheckbox], + ); - return ; + return ( + + ); }; export default BoltEmbeddedPaymentMethod; diff --git a/packages/core/src/app/payment/paymentMethod/BoltPaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/BoltPaymentMethod.spec.tsx index a0cb765fbd..15b031fd4a 100644 --- a/packages/core/src/app/payment/paymentMethod/BoltPaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/BoltPaymentMethod.spec.tsx @@ -27,9 +27,7 @@ describe('when using Bolt payment', () => { }, }; - PaymentMethodTest = props => ( - - ); + PaymentMethodTest = (props) => ; }); it('renders as bolt client payment method if embeddedOneClickEnabled is false', () => { @@ -37,8 +35,8 @@ describe('when using Bolt payment', () => { const container = mount(); - expect(container.find(BoltClientPaymentMethod).length).toEqual(1); - expect(container.find(HostedPaymentMethod).length).toEqual(1); + expect(container.find(BoltClientPaymentMethod)).toHaveLength(1); + expect(container.find(HostedPaymentMethod)).toHaveLength(1); }); it('renders as bolt embedded payment method if embeddedOneClickEnabled is true', () => { @@ -46,7 +44,7 @@ describe('when using Bolt payment', () => { const container = mount(); - expect(container.find(BoltEmbeddedPaymentMethod).length).toEqual(1); - expect(container.find(HostedWidgetPaymentMethod).length).toEqual(1); + expect(container.find(BoltEmbeddedPaymentMethod)).toHaveLength(1); + expect(container.find(HostedWidgetPaymentMethod)).toHaveLength(1); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/BoltPaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/BoltPaymentMethod.tsx index ac54c6b842..c19e7a0e27 100644 --- a/packages/core/src/app/payment/paymentMethod/BoltPaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/BoltPaymentMethod.tsx @@ -4,14 +4,14 @@ import BoltClientPaymentMethod from './BoltClientPaymentMethod'; import BoltEmbeddedPaymentMethod from './BoltEmbeddedPaymentMethod'; import { HostedPaymentMethodProps } from './HostedPaymentMethod'; -const BoltPaymentMethod: FunctionComponent = props => { - const useBoltEmbedded = props?.method?.initializationData?.embeddedOneClickEnabled; +const BoltPaymentMethod: FunctionComponent = (props) => { + const useBoltEmbedded = props.method.initializationData?.embeddedOneClickEnabled; if (useBoltEmbedded) { - return ; + return ; } - return ; + return ; }; export default BoltPaymentMethod; diff --git a/packages/core/src/app/payment/paymentMethod/BraintreeCreditCardPaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/BraintreeCreditCardPaymentMethod.spec.tsx index 80f4c34f7b..ee885b865f 100644 --- a/packages/core/src/app/payment/paymentMethod/BraintreeCreditCardPaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/BraintreeCreditCardPaymentMethod.spec.tsx @@ -1,8 +1,12 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, +} from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import { Formik } from 'formik'; import { noop } from 'lodash'; -import React, { useEffect, FunctionComponent } from 'react'; +import React, { FunctionComponent, useEffect } from 'react'; import { act } from 'react-dom/test-utils'; import { object } from 'yup'; @@ -12,11 +16,16 @@ import { getStoreConfig } from '../../config/config.mock'; import { getCustomer } from '../../customer/customers.mock'; import { createLocaleContext, LocaleContext, LocaleContextType } from '../../locale'; import { Modal, ModalProps } from '../../ui/modal'; -import { withHostedCreditCardFieldset, WithInjectedHostedCreditCardFieldsetProps } from '../hostedCreditCard'; +import { + withHostedCreditCardFieldset, + WithInjectedHostedCreditCardFieldsetProps, +} from '../hostedCreditCard'; import { getPaymentMethod } from '../payment-methods.mock'; import PaymentContext, { PaymentContextProps } from '../PaymentContext'; -import BraintreeCreditCardPaymentMethod, { BraintreeCreditCardPaymentMethodProps } from './BraintreeCreditCardPaymentMethod'; +import BraintreeCreditCardPaymentMethod, { + BraintreeCreditCardPaymentMethodProps, +} from './BraintreeCreditCardPaymentMethod'; import CreditCardPaymentMethod from './CreditCardPaymentMethod'; import PaymentMethodId from './PaymentMethodId'; @@ -39,28 +48,24 @@ const injectedProps: WithInjectedHostedCreditCardFieldsetProps = { jest.mock('../hostedCreditCard', () => ({ ...jest.requireActual('../hostedCreditCard'), - withHostedCreditCardFieldset: jest.fn( - Component => (props: any) => - ) as jest.Mocked, + withHostedCreditCardFieldset: jest.fn((Component) => (props: any) => ( + + )) as jest.Mocked, })); -jest.mock('./CreditCardPaymentMethod', () => - jest.fn(({ - initializePayment, - method, - }) => { - useEffect(() => { - initializePayment({ - methodId: method.id, - gatewayId: method.gateway, +jest.mock( + './CreditCardPaymentMethod', + () => + jest.fn(({ initializePayment, method }) => { + useEffect(() => { + initializePayment({ + methodId: method.id, + gatewayId: method.gateway, + }); }); - }); - return
    ; - }) as jest.Mocked + return
    ; + }) as jest.Mocked, ); describe('when using Braintree payment', () => { @@ -93,30 +98,22 @@ describe('when using Braintree payment', () => { hidePaymentSubmitButton: jest.fn(), }; - jest.spyOn(checkoutState.data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(checkoutState.data, 'getCart').mockReturnValue(getCart()); - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - jest.spyOn(checkoutState.data, 'getCustomer') - .mockReturnValue(getCustomer()); + jest.spyOn(checkoutState.data, 'getCustomer').mockReturnValue(getCustomer()); - jest.spyOn(checkoutService, 'deinitializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'deinitializePayment').mockResolvedValue(checkoutState); - jest.spyOn(checkoutService, 'initializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'initializePayment').mockResolvedValue(checkoutState); - BraintreeCreditCardPaymentMethodTest = props => ( - - - - - + BraintreeCreditCardPaymentMethodTest = (props) => ( + + + + + @@ -125,25 +122,26 @@ describe('when using Braintree payment', () => { }); it('renders as credit card payment method', async () => { - const container = mount(); + const container = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(container.find(CreditCardPaymentMethod).props()) - .toEqual(expect.objectContaining({ + expect(container.find(CreditCardPaymentMethod).props()).toEqual( + expect.objectContaining({ deinitializePayment: expect.any(Function), initializePayment: expect.any(Function), method: defaultProps.method, - })); + }), + ); }); it('initializes method with required config', async () => { - mount(); + mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(defaultProps.initializePayment) - .toHaveBeenCalledWith(expect.objectContaining({ + expect(defaultProps.initializePayment).toHaveBeenCalledWith( + expect.objectContaining({ methodId: defaultProps.method.id, gatewayId: defaultProps.method.gateway, braintree: { @@ -153,30 +151,33 @@ describe('when using Braintree payment', () => { }, form: hostedFormOptions, }, - })); + }), + ); }); it('injects hosted form properties to credit card payment method component', async () => { - const component = mount(); + const component = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); const decoratedComponent = component.find(CreditCardPaymentMethod); - expect(decoratedComponent.prop('cardFieldset')) - .toEqual(injectedProps.hostedFieldset); - expect(decoratedComponent.prop('cardValidationSchema')) - .toEqual(injectedProps.hostedValidationSchema); - expect(decoratedComponent.prop('getStoredCardValidationFieldset')) - .toEqual(injectedProps.getHostedStoredCardValidationFieldset); - expect(decoratedComponent.prop('storedCardValidationSchema')) - .toEqual(injectedProps.hostedStoredCardValidationSchema); + expect(decoratedComponent.prop('cardFieldset')).toEqual(injectedProps.hostedFieldset); + expect(decoratedComponent.prop('cardValidationSchema')).toEqual( + injectedProps.hostedValidationSchema, + ); + expect(decoratedComponent.prop('getStoredCardValidationFieldset')).toEqual( + injectedProps.getHostedStoredCardValidationFieldset, + ); + expect(decoratedComponent.prop('storedCardValidationSchema')).toEqual( + injectedProps.hostedStoredCardValidationSchema, + ); }); it('renders 3DS modal if required by selected method', async () => { - const component = mount(); + const component = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); const initializeOptions = (defaultProps.initializePayment as jest.Mock).mock.calls[0][0]; @@ -184,25 +185,24 @@ describe('when using Braintree payment', () => { initializeOptions.braintree.threeDSecure.addFrame( undefined, document.createElement('iframe'), - jest.fn() + jest.fn(), ); }); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); act(() => { component.update(); }); - expect(component.find(Modal).prop('isOpen')) - .toEqual(true); + expect(component.find(Modal).prop('isOpen')).toBe(true); }); it('cancels 3DS modal flow if user chooses to close modal', async () => { const cancelThreeDSecureVerification = jest.fn(); - const component = mount(); + const component = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); const initializeOptions = (defaultProps.initializePayment as jest.Mock).mock.calls[0][0]; @@ -210,11 +210,11 @@ describe('when using Braintree payment', () => { initializeOptions.braintree.threeDSecure.addFrame( undefined, document.createElement('iframe'), - cancelThreeDSecureVerification + cancelThreeDSecureVerification, ); }); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); act(() => { component.update(); @@ -227,7 +227,6 @@ describe('when using Braintree payment', () => { modal.prop('onRequestClose')!(new MouseEvent('click') as any); }); - expect(cancelThreeDSecureVerification) - .toHaveBeenCalled(); + expect(cancelThreeDSecureVerification).toHaveBeenCalled(); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/BraintreeCreditCardPaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/BraintreeCreditCardPaymentMethod.tsx index 2cb419c927..59fe9336e1 100644 --- a/packages/core/src/app/payment/paymentMethod/BraintreeCreditCardPaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/BraintreeCreditCardPaymentMethod.tsx @@ -1,9 +1,19 @@ import { noop } from 'lodash'; -import React, { createRef, useCallback, useRef, useState, FunctionComponent, RefObject } from 'react'; +import React, { + createRef, + FunctionComponent, + RefObject, + useCallback, + useRef, + useState, +} from 'react'; import { TranslatedString } from '../../locale'; import { Modal } from '../../ui/modal'; -import { withHostedCreditCardFieldset, WithInjectedHostedCreditCardFieldsetProps } from '../hostedCreditCard'; +import { + withHostedCreditCardFieldset, + WithInjectedHostedCreditCardFieldsetProps, +} from '../hostedCreditCard'; import CreditCardPaymentMethod, { CreditCardPaymentMethodProps } from './CreditCardPaymentMethod'; @@ -15,8 +25,7 @@ interface BraintreeCreditCardPaymentMethodRef { } const BraintreeCreditCardPaymentMethod: FunctionComponent< - BraintreeCreditCardPaymentMethodProps & - WithInjectedHostedCreditCardFieldsetProps + BraintreeCreditCardPaymentMethodProps & WithInjectedHostedCreditCardFieldsetProps > = ({ getHostedFormOptions, getHostedStoredCardValidationFieldset, @@ -32,32 +41,34 @@ const BraintreeCreditCardPaymentMethod: FunctionComponent< threeDSecureContentRef: createRef(), }); - const initializeBraintreePayment: BraintreeCreditCardPaymentMethodProps['initializePayment'] = useCallback(async (options, selectedInstrument) => { - return initializePayment({ - ...options, - braintree: { - threeDSecure: { - addFrame(error, content, cancel) { - if (error) { - return onUnhandledError(error); - } + const initializeBraintreePayment: BraintreeCreditCardPaymentMethodProps['initializePayment'] = + useCallback( + async (options, selectedInstrument) => { + return initializePayment({ + ...options, + braintree: { + threeDSecure: { + addFrame(error, content, cancel) { + if (error) { + return onUnhandledError(error); + } - setThreeDSecureContent(content); - ref.current.cancelThreeDSecureVerification = cancel; + setThreeDSecureContent(content); + ref.current.cancelThreeDSecureVerification = cancel; + }, + removeFrame() { + setThreeDSecureContent(undefined); + ref.current.cancelThreeDSecureVerification = undefined; + }, + }, + form: + getHostedFormOptions && + (await getHostedFormOptions(selectedInstrument)), }, - removeFrame() { - setThreeDSecureContent(undefined); - ref.current.cancelThreeDSecureVerification = undefined; - }, - }, - form: getHostedFormOptions && await getHostedFormOptions(selectedInstrument), + }); }, - }); - }, [ - getHostedFormOptions, - initializePayment, - onUnhandledError, - ]); + [getHostedFormOptions, initializePayment, onUnhandledError], + ); const appendThreeDSecureContent = useCallback(() => { if (ref.current.threeDSecureContentRef.current && threeDSecureContent) { @@ -74,26 +85,28 @@ const BraintreeCreditCardPaymentMethod: FunctionComponent< } }, []); - return <> - + return ( + <> + - } - isOpen={ !!threeDSecureContent } - onAfterOpen={ appendThreeDSecureContent } - onRequestClose={ cancelThreeDSecureModalFlow } - > -
    - - ; + } + isOpen={!!threeDSecureContent} + onAfterOpen={appendThreeDSecureContent} + onRequestClose={cancelThreeDSecureModalFlow} + > +
    + + + ); }; export default withHostedCreditCardFieldset(BraintreeCreditCardPaymentMethod); diff --git a/packages/core/src/app/payment/paymentMethod/CCAvenueMarsPaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/CCAvenueMarsPaymentMethod.spec.tsx index bde76535e5..caa948677a 100644 --- a/packages/core/src/app/payment/paymentMethod/CCAvenueMarsPaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/CCAvenueMarsPaymentMethod.spec.tsx @@ -3,7 +3,9 @@ import React from 'react'; import { getPaymentMethod } from '../payment-methods.mock'; -import CCAvenueMarsPaymentMethod, { CCAvenueMarsPaymentMethodProps } from './CCAvenueMarsPaymentMethod'; +import CCAvenueMarsPaymentMethod, { + CCAvenueMarsPaymentMethodProps, +} from './CCAvenueMarsPaymentMethod'; import HostedWidgetPaymentMethod from './HostedWidgetPaymentMethod'; import PaymentMethodId from './PaymentMethodId'; @@ -19,7 +21,8 @@ describe('When using CCAvenueMars Payment Method', () => { }; it('Shopper is able to see CCAvenueMars Payment Method', () => { - const component = mount(); + const component = mount(); + expect(component.find(HostedWidgetPaymentMethod)).toBeTruthy(); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/CCAvenueMarsPaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/CCAvenueMarsPaymentMethod.tsx index 69bfcef04c..ef93b4b16d 100644 --- a/packages/core/src/app/payment/paymentMethod/CCAvenueMarsPaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/CCAvenueMarsPaymentMethod.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, FunctionComponent } from 'react'; +import React, { FunctionComponent, useMemo } from 'react'; import { Omit } from 'utility-types'; import { TranslatedString } from '../../locale'; @@ -7,13 +7,13 @@ import HostedPaymentMethod, { HostedPaymentMethodProps } from './HostedPaymentMe export type CCAvenueMarsPaymentMethodProps = Omit; -const CCAvenueMarsPaymentMethod: FunctionComponent = props => { - const description = useMemo(() => , []); +const CCAvenueMarsPaymentMethod: FunctionComponent = (props) => { + const description = useMemo( + () => , + [], + ); - return ; + return ; }; export default CCAvenueMarsPaymentMethod; diff --git a/packages/core/src/app/payment/paymentMethod/ChasePayPaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/ChasePayPaymentMethod.spec.tsx index c4de9eae32..d2ae8986ad 100644 --- a/packages/core/src/app/payment/paymentMethod/ChasePayPaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/ChasePayPaymentMethod.spec.tsx @@ -1,4 +1,9 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService, PaymentMethod } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, + PaymentMethod, +} from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import { Formik } from 'formik'; import { noop } from 'lodash'; @@ -12,7 +17,9 @@ import { getPaymentMethod } from '../payment-methods.mock'; import { default as PaymentMethodComponent, PaymentMethodProps } from './PaymentMethod'; import PaymentMethodId from './PaymentMethodId'; import PaymentMethodType from './PaymentMethodType'; -import WalletButtonPaymentMethod, { WalletButtonPaymentMethodProps } from './WalletButtonPaymentMethod'; +import WalletButtonPaymentMethod, { + WalletButtonPaymentMethodProps, +} from './WalletButtonPaymentMethod'; describe('when using ChasePay payment', () => { let method: PaymentMethod; @@ -37,23 +44,17 @@ describe('when using ChasePay payment', () => { method: PaymentMethodType.Chasepay, }; - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - jest.spyOn(checkoutService, 'deinitializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'deinitializePayment').mockResolvedValue(checkoutState); - jest.spyOn(checkoutService, 'initializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'initializePayment').mockResolvedValue(checkoutState); - PaymentMethodTest = props => ( - - - - + PaymentMethodTest = (props) => ( + + + + @@ -61,34 +62,37 @@ describe('when using ChasePay payment', () => { }); it('renders as wallet button method', () => { - const container = mount(); + const container = mount(); - expect(container.find(WalletButtonPaymentMethod).props()) - .toEqual(expect.objectContaining({ + expect(container.find(WalletButtonPaymentMethod).props()).toEqual( + expect.objectContaining({ buttonId: 'chaseWalletButton', deinitializePayment: expect.any(Function), initializePayment: expect.any(Function), method, shouldShowEditButton: true, - })); + }), + ); }); it('initializes method with required config', () => { - const container = mount(); - const component: ReactWrapper = container.find(WalletButtonPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(WalletButtonPaymentMethod); component.prop('initializePayment')({ methodId: method.id, gatewayId: method.gateway, }); - expect(checkoutService.initializePayment) - .toHaveBeenCalledWith(expect.objectContaining({ + expect(checkoutService.initializePayment).toHaveBeenCalledWith( + expect.objectContaining({ methodId: method.id, gatewayId: method.gateway, [method.id]: { walletButton: 'chaseWalletButton', }, - })); + }), + ); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/ChasePayPaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/ChasePayPaymentMethod.tsx index 5dbd793810..4bd0958d43 100644 --- a/packages/core/src/app/payment/paymentMethod/ChasePayPaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/ChasePayPaymentMethod.tsx @@ -1,28 +1,39 @@ import { PaymentInitializeOptions } from '@bigcommerce/checkout-sdk'; -import React, { useCallback, FunctionComponent } from 'react'; +import React, { FunctionComponent, useCallback } from 'react'; import { Omit } from 'utility-types'; -import WalletButtonPaymentMethod, { WalletButtonPaymentMethodProps } from './WalletButtonPaymentMethod'; +import WalletButtonPaymentMethod, { + WalletButtonPaymentMethodProps, +} from './WalletButtonPaymentMethod'; -export type CCAvenueMarsPaymentMethodProps = Omit; +export type CCAvenueMarsPaymentMethodProps = Omit< + WalletButtonPaymentMethodProps, + 'buttonId' | 'shouldShowEditButton' +>; const ChasePayPaymentMethod: FunctionComponent = ({ initializePayment, ...rest }) => { - const initializeChasePayPayment = useCallback((options: PaymentInitializeOptions) => initializePayment({ - ...options, - chasepay: { - walletButton: 'chaseWalletButton', - }, - }), [initializePayment]); + const initializeChasePayPayment = useCallback( + (options: PaymentInitializeOptions) => + initializePayment({ + ...options, + chasepay: { + walletButton: 'chaseWalletButton', + }, + }), + [initializePayment], + ); - return ; + return ( + + ); }; export default ChasePayPaymentMethod; diff --git a/packages/core/src/app/payment/paymentMethod/CheckoutcomCustomFormFields.spec.tsx b/packages/core/src/app/payment/paymentMethod/CheckoutcomCustomFormFields.spec.tsx index 9ef2f56e91..1ca73fa184 100644 --- a/packages/core/src/app/payment/paymentMethod/CheckoutcomCustomFormFields.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/CheckoutcomCustomFormFields.spec.tsx @@ -1,4 +1,8 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, +} from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import { EventEmitter } from 'events'; import { Formik } from 'formik'; @@ -16,8 +20,15 @@ import { DropdownTrigger } from '../../ui/dropdown'; import { FormContext, FormContextType } from '../../ui/form'; import PaymentContext, { PaymentContextProps } from '../PaymentContext'; -import checkoutcomCustomFormFields, { ccDocumentField, HiddenInput, OptionButton } from './CheckoutcomCustomFormFields'; -import CreditCardPaymentMethod, { CreditCardPaymentMethodProps, CreditCardPaymentMethodValues } from './CreditCardPaymentMethod'; +import checkoutcomCustomFormFields, { + ccDocumentField, + HiddenInput, + OptionButton, +} from './CheckoutcomCustomFormFields'; +import CreditCardPaymentMethod, { + CreditCardPaymentMethodProps, + CreditCardPaymentMethodValues, +} from './CreditCardPaymentMethod'; const getAPMProps = { ideal: () => ({ @@ -32,9 +43,16 @@ const getAPMProps = { config: { cardCode: null, displayName: 'iDEAL' }, type: 'PAYMENT_TYPE_API', nonce: null, - initializationData: { gateway: 'checkoutcom', idealIssuers: [{ bic: 'INGBNL2A', name: 'Issuer Simulation V3 - ING' }, { bic: 'RABONL2U', name: 'Issuer Simulation V3 - RABO' }] }, + initializationData: { + gateway: 'checkoutcom', + idealIssuers: [ + { bic: 'INGBNL2A', name: 'Issuer Simulation V3 - ING' }, + { bic: 'RABONL2U', name: 'Issuer Simulation V3 - RABO' }, + ], + }, clientToken: null, - returnUrl: 'https://test-store.store.bcdev/checkout.php?action=set_external_checkout&provider=checkoutcom', + returnUrl: + 'https://test-store.store.bcdev/checkout.php?action=set_external_checkout&provider=checkoutcom', }, }), sepa: () => ({ @@ -49,7 +67,8 @@ const getAPMProps = { config: { cardCode: null, displayName: 'SEPA' }, type: 'PAYMENT_TYPE_API', nonce: null, - initializationData: { gateway: 'checkoutcom', + initializationData: { + gateway: 'checkoutcom', sepaCreditor: { sepaCreditorAddress: 'sepaCreditorAddress', sepaCreditorCity: 'sepaCreditorCity', @@ -57,9 +76,11 @@ const getAPMProps = { sepaCreditorCountry: 'sepaCreditorCountry', sepaCreditorIdentifier: 'sepaCreditorIdentifier', sepaCreditorPostalCode: 'sepaCreditorPostalCode', - }}, + }, + }, clientToken: null, - returnUrl: 'https://test-store.store.bcdev/checkout.php?action=set_external_checkout&provider=checkoutcom', + returnUrl: + 'https://test-store.store.bcdev/checkout.php?action=set_external_checkout&provider=checkoutcom', }, }), oxxo: () => ({ @@ -76,7 +97,8 @@ const getAPMProps = { nonce: null, initializationData: { gateway: 'checkoutcom' }, clientToken: null, - returnUrl: 'https://test-store.store.bcdev/checkout.php?action=set_external_checkout&provider=checkoutcom', + returnUrl: + 'https://test-store.store.bcdev/checkout.php?action=set_external_checkout&provider=checkoutcom', }, }), fawry: () => ({ @@ -93,7 +115,8 @@ const getAPMProps = { nonce: null, initializationData: { gateway: 'checkoutcom' }, clientToken: null, - returnUrl: 'https://test-store.store.bcdev/checkout.php?action=set_external_checkout&provider=checkoutcom', + returnUrl: + 'https://test-store.store.bcdev/checkout.php?action=set_external_checkout&provider=checkoutcom', }, }), }; @@ -101,13 +124,20 @@ const getAPMProps = { describe('CheckoutCustomFormFields', () => { let checkoutService: CheckoutService; let checkoutState: CheckoutSelectors; - let defaultProps: jest.Mocked>; + let defaultProps: jest.Mocked< + Omit + >; let formContext: FormContextType; let initialValues: CreditCardPaymentMethodValues; let localeContext: LocaleContextType; let paymentContext: PaymentContextProps; let subscribeEventEmitter: EventEmitter; - let CheckoutcomAPMsTest: FunctionComponent>; + let CheckoutcomAPMsTest: FunctionComponent< + Omit< + CreditCardPaymentMethodProps, + 'cardValidationSchema' | 'deinitializePayment' | 'initializePayment' + > + >; beforeEach(() => { defaultProps = { @@ -139,49 +169,37 @@ describe('CheckoutCustomFormFields', () => { }; subscribeEventEmitter = new EventEmitter(); - jest.spyOn(checkoutService, 'getState') - .mockReturnValue(checkoutState); + jest.spyOn(checkoutService, 'getState').mockReturnValue(checkoutState); - jest.spyOn(checkoutService, 'subscribe') - .mockImplementation(subscriber => { - subscribeEventEmitter.on('change', () => subscriber(checkoutState)); - subscribeEventEmitter.emit('change'); + jest.spyOn(checkoutService, 'subscribe').mockImplementation((subscriber) => { + subscribeEventEmitter.on('change', () => subscriber(checkoutState)); + subscribeEventEmitter.emit('change'); - return noop; - }); + return noop; + }); - jest.spyOn(checkoutService, 'loadInstruments') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'loadInstruments').mockResolvedValue(checkoutState); - jest.spyOn(checkoutState.data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(checkoutState.data, 'getCart').mockReturnValue(getCart()); - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - jest.spyOn(checkoutState.data, 'getConsignments') - .mockReturnValue([getConsignment()]); + jest.spyOn(checkoutState.data, 'getConsignments').mockReturnValue([getConsignment()]); - jest.spyOn(checkoutState.data, 'getCustomer') - .mockReturnValue(getCustomer()); + jest.spyOn(checkoutState.data, 'getCustomer').mockReturnValue(getCustomer()); - jest.spyOn(checkoutState.data, 'getInstruments') - .mockReturnValue([]); + jest.spyOn(checkoutState.data, 'getInstruments').mockReturnValue([]); - jest.spyOn(checkoutState.data, 'isPaymentDataRequired') - .mockReturnValue(true); + jest.spyOn(checkoutState.data, 'isPaymentDataRequired').mockReturnValue(true); - CheckoutcomAPMsTest = props => { + CheckoutcomAPMsTest = (props) => { return ( - - - - - - + + + + + + @@ -197,7 +215,14 @@ describe('CheckoutCustomFormFields', () => { const sepaProps = getAPMProps.sepa(); beforeEach(() => { - component = mount( } />); + component = mount( + + } + />, + ); }); it('should render the sepa fieldset', () => { @@ -207,15 +232,16 @@ describe('CheckoutCustomFormFields', () => { it('should call toggleSubmitButton on checkbox checked', () => { const checkbox = component.find('input[name="sepaMandate"]'); - checkbox.simulate('change', {target: {name: 'sepaMandate', value: true}}); - expect(paymentContext.disableSubmit).lastCalledWith(sepaProps.method, false); + checkbox.simulate('change', { target: { name: 'sepaMandate', value: true } }); + + expect(paymentContext.disableSubmit).toHaveBeenLastCalledWith(sepaProps.method, false); }); it('should call disableSubmit on useEffect cleanup function', () => { component.unmount(); - expect(paymentContext.disableSubmit).lastCalledWith(sepaProps.method, false); + expect(paymentContext.disableSubmit).toHaveBeenLastCalledWith(sepaProps.method, false); }); afterEach(() => { @@ -229,11 +255,16 @@ describe('CheckoutCustomFormFields', () => { beforeEach(() => { const idealProps = getAPMProps.ideal(); - component = mount( } />); + + component = mount( + } + />, + ); }); it('Shopper is able to see iDeal Payment Method', () => { - expect(component.find('input[type="hidden"]')).toHaveLength(1); }); @@ -241,9 +272,11 @@ describe('CheckoutCustomFormFields', () => { component.find(DropdownTrigger).simulate('click'); component.find(OptionButton).at(0).simulate('click'); - expect(component.find(HiddenInput).props()).toEqual(expect.objectContaining({ - selectedIssuer: 'INGBNL2A', - })); + expect(component.find(HiddenInput).props()).toEqual( + expect.objectContaining({ + selectedIssuer: 'INGBNL2A', + }), + ); }); }); @@ -252,7 +285,12 @@ describe('CheckoutCustomFormFields', () => { it('should render the ideal fieldset', () => { const oxxoProps = getAPMProps.oxxo(); - const compoonent = mount( } />); + const compoonent = mount( + } + />, + ); expect(compoonent.find('input[name="ccDocument"]')).toHaveLength(1); }); @@ -263,7 +301,12 @@ describe('CheckoutCustomFormFields', () => { it('Shopper is able to see Fawry Payment Method', () => { const fawryProps = getAPMProps.fawry(); - const component = mount( } />); + const component = mount( + } + />, + ); expect(component.find('input[name="customerMobile"]')).toHaveLength(1); expect(component.find('input[name="customerEmail"]')).toHaveLength(1); diff --git a/packages/core/src/app/payment/paymentMethod/CheckoutcomCustomFormFields.tsx b/packages/core/src/app/payment/paymentMethod/CheckoutcomCustomFormFields.tsx index af6303ef9b..034a47cd21 100644 --- a/packages/core/src/app/payment/paymentMethod/CheckoutcomCustomFormFields.tsx +++ b/packages/core/src/app/payment/paymentMethod/CheckoutcomCustomFormFields.tsx @@ -1,6 +1,13 @@ import { BillingAddress, PaymentMethod } from '@bigcommerce/checkout-sdk'; import { FieldProps } from 'formik'; -import React, { useCallback, useContext, useEffect, useState, FunctionComponent, SyntheticEvent } from 'react'; +import React, { + FunctionComponent, + SyntheticEvent, + useCallback, + useContext, + useEffect, + useState, +} from 'react'; import { TranslatedString } from '../../locale'; import { DropdownTrigger } from '../../ui/dropdown'; @@ -12,16 +19,20 @@ interface CheckoutcomAPMFormProps { method: PaymentMethod; debtor: BillingAddress; } + interface Issuer { bic: string; name: string; } + interface HiddenInputProps extends FieldProps { selectedIssuer?: string; } + interface DropdownButtonProps { selectedIssuer?: Issuer; } + interface OptionButtonProps { className?: string; issuer: Issuer; @@ -37,7 +48,7 @@ interface SepaCreditor { sepaCreditorPostalCode: string; } -const Sepa: FunctionComponent = ({method, debtor}) => { +const Sepa: FunctionComponent = ({ method, debtor }) => { const paymentContext = useContext(PaymentContext); const creditor: SepaCreditor = method.initializationData.sepaCreditor; @@ -51,50 +62,58 @@ const Sepa: FunctionComponent = ({method, debtor}) => { paymentContext?.disableSubmit(method, !isChecked); } - return (<> -
    -
    -

    - -

    -
    { creditor.sepaCreditorCompanyName }
    -

    { creditor.sepaCreditorAddress }

    -

    { `${creditor.sepaCreditorPostalCode} ${creditor.sepaCreditorCity}` }

    -

    { creditor.sepaCreditorCountry }

    -
    -

    - -

    -
    -
    -

    - -

    -
    { `${debtor.firstName} ${debtor.lastName}` }
    -

    { debtor.address1 }

    -

    { `${debtor.postalCode} ${debtor.city}, ${debtor.stateOrProvinceCode}` }

    -

    { debtor.countryCode }

    + return ( + <> +
    +
    +

    + +

    +
    {creditor.sepaCreditorCompanyName}
    +

    {creditor.sepaCreditorAddress}

    +

    {`${creditor.sepaCreditorPostalCode} ${creditor.sepaCreditorCity}`}

    +

    {creditor.sepaCreditorCountry}

    +
    +

    + +

    +
    +
    +

    + +

    +
    {`${debtor.firstName} ${debtor.lastName}`}
    +

    {debtor.address1}

    +

    {`${debtor.postalCode} ${debtor.city}, ${debtor.stateOrProvinceCode}`}

    +

    {debtor.countryCode}

    +
    -
    -

    - -

    -
    - - - - } - name="sepaMandate" - onChange={ toggleSubmitButton } - /> - ); +

    + +

    +
    + + + + } + name="sepaMandate" + onChange={toggleSubmitButton} + /> + + ); }; const Fawry: FunctionComponent = () => { @@ -119,12 +138,16 @@ const Fawry: FunctionComponent = () => { const Ideal: FunctionComponent = ({ method }) => { const [selectedIssuer, setSelectedIssuer] = useState(); const [bicValue, setBicValue] = useState(''); - const render = useCallback((props: FieldProps) => , [bicValue]); + const render = useCallback( + (props: FieldProps) => , + [bicValue], + ); const issuers: Issuer[] = method.initializationData.idealIssuers; const handleClick = ({ currentTarget }: SyntheticEvent) => { - const _selectedIssuer = issuers.find(({ bic }) => bic === currentTarget?.dataset.bic); + const _selectedIssuer = issuers.find(({ bic }) => bic === currentTarget.dataset.bic); + if (!_selectedIssuer) { return; } @@ -133,26 +156,33 @@ const Ideal: FunctionComponent = ({ method }) => { setBicValue(_selectedIssuer.bic); }; - const issuersList =
      - { issuers.map(issuer => -
    • - -
    • - ) } -
    ; - - return (<> - - - - - ); + const issuersList = ( +
      + {issuers.map((issuer) => ( +
    • + +
    • + ))} +
    + ); + + return ( + <> + + + + + + ); }; -export const HiddenInput: FunctionComponent = ({field: {value, ...restField}, form, selectedIssuer}) => { - const Input = useCallback(() => , [restField]); +export const HiddenInput: FunctionComponent = ({ + field: { value, ...restField }, + form, + selectedIssuer, +}) => { + const Input = useCallback(() => , [restField]); + useEffect(() => { if (value === selectedIssuer) { return; @@ -166,32 +196,34 @@ export const HiddenInput: FunctionComponent = ({field: {value, const DropdownButton: FunctionComponent = ({ selectedIssuer }) => { if (!selectedIssuer) { - return (
    - ); + + ); } - return (); + return ( + + ); }; export const OptionButton: FunctionComponent = ({ issuer, ...restProps }) => { const { bic, name } = issuer; - return (); + return ( + + ); }; const checkoutcomCustomFormFields = { @@ -204,7 +236,7 @@ export const ccDocumentField = ({ method }: CheckoutcomAPMFormProps) => ( ); diff --git a/packages/core/src/app/payment/paymentMethod/CheckoutcomCustomPaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/CheckoutcomCustomPaymentMethod.tsx index cf8bc18b0c..6ba267e35d 100644 --- a/packages/core/src/app/payment/paymentMethod/CheckoutcomCustomPaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/CheckoutcomCustomPaymentMethod.tsx @@ -1,9 +1,13 @@ import { BillingAddress } from '@bigcommerce/checkout-sdk'; import React, { FunctionComponent } from 'react'; -import { withCheckout, CheckoutContextProps } from '../../checkout'; +import { CheckoutContextProps, withCheckout } from '../../checkout'; import { withLanguage, WithLanguageProps } from '../../locale'; -import { checkoutcomCustomPaymentMethods, checkoutcomPaymentMethods, getCheckoutcomValidationSchemas } from '../checkoutcomFieldsets'; +import { + checkoutcomCustomPaymentMethods, + checkoutcomPaymentMethods, + getCheckoutcomValidationSchemas, +} from '../checkoutcomFieldsets'; import checkoutcomCustomFormFields, { ccDocumentField } from './CheckoutcomCustomFormFields'; import CreditCardPaymentMethod, { CreditCardPaymentMethodProps } from './CreditCardPaymentMethod'; @@ -18,29 +22,33 @@ interface WithCheckoutCheckoutcomCustomPaymentMethodProps { } const CheckoutcomCustomPaymentMethod: FunctionComponent< - CheckoutcomCustomPaymentMethodProps & WithCheckoutCheckoutcomCustomPaymentMethodProps & WithLanguageProps + CheckoutcomCustomPaymentMethodProps & + WithCheckoutCheckoutcomCustomPaymentMethodProps & + WithLanguageProps > = ({ language, checkoutCustomMethod, ...rest }) => { - - const CheckoutcomCustomFieldset = checkoutCustomMethod in checkoutcomCustomFormFields - ? checkoutcomCustomFormFields[checkoutCustomMethod as checkoutcomCustomPaymentMethods] - : ccDocumentField; + const CheckoutcomCustomFieldset = + checkoutCustomMethod in checkoutcomCustomFormFields + ? checkoutcomCustomFormFields[checkoutCustomMethod as checkoutcomCustomPaymentMethods] + : ccDocumentField; return ( } - cardValidationSchema={ getCheckoutcomValidationSchemas({ + {...rest} + cardFieldset={} + cardValidationSchema={getCheckoutcomValidationSchemas({ paymentMethod: checkoutCustomMethod as checkoutcomPaymentMethods, language, - }) } + })} /> ); }; -function mapToCheckoutcomCustomPaymentMethodProps( - { checkoutState }: CheckoutContextProps -): WithCheckoutCheckoutcomCustomPaymentMethodProps { - const { data: { getBillingAddress } } = checkoutState; +function mapToCheckoutcomCustomPaymentMethodProps({ + checkoutState, +}: CheckoutContextProps): WithCheckoutCheckoutcomCustomPaymentMethodProps { + const { + data: { getBillingAddress }, + } = checkoutState; const billingAddress = getBillingAddress(); if (!billingAddress) { @@ -52,4 +60,6 @@ function mapToCheckoutcomCustomPaymentMethodProps( }; } -export default withLanguage(withCheckout(mapToCheckoutcomCustomPaymentMethodProps)(CheckoutcomCustomPaymentMethod)); +export default withLanguage( + withCheckout(mapToCheckoutcomCustomPaymentMethodProps)(CheckoutcomCustomPaymentMethod), +); diff --git a/packages/core/src/app/payment/paymentMethod/CreditCardFieldsetValues.ts b/packages/core/src/app/payment/paymentMethod/CreditCardFieldsetValues.ts index b490c77c47..b91a63aaf1 100644 --- a/packages/core/src/app/payment/paymentMethod/CreditCardFieldsetValues.ts +++ b/packages/core/src/app/payment/paymentMethod/CreditCardFieldsetValues.ts @@ -1,4 +1,4 @@ -import { CardInstrumentFieldsetValues } from "../storedInstrument"; +import { CardInstrumentFieldsetValues } from '../storedInstrument'; export default interface CreditCardFieldsetValues { ccCustomerCode?: string; diff --git a/packages/core/src/app/payment/paymentMethod/CreditCardPaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/CreditCardPaymentMethod.spec.tsx index 9fbb78a1f2..5a7f4f8269 100644 --- a/packages/core/src/app/payment/paymentMethod/CreditCardPaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/CreditCardPaymentMethod.spec.tsx @@ -1,10 +1,14 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, +} from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import { EventEmitter } from 'events'; import { Formik } from 'formik'; import { merge, noop } from 'lodash'; import React, { FunctionComponent } from 'react'; -import { object, string, Schema } from 'yup'; +import { object, Schema, string } from 'yup'; import { getCart } from '../../cart/carts.mock'; import { CheckoutProvider } from '../../checkout'; @@ -14,19 +18,27 @@ import { createLocaleContext, LocaleContext, LocaleContextType } from '../../loc import { getConsignment } from '../../shipping/consignment.mock'; import { FormContext, FormContextType } from '../../ui/form'; import { LoadingOverlay } from '../../ui/loading'; -import { getCreditCardValidationSchema, CreditCardFieldset } from '../creditCard'; +import { CreditCardFieldset, getCreditCardValidationSchema } from '../creditCard'; import { getPaymentMethod } from '../payment-methods.mock'; -import { getInstrumentValidationSchema, isInstrumentFeatureAvailable, CardInstrumentFieldset } from '../storedInstrument'; -import { getCardInstrument, getInstruments } from '../storedInstrument/instruments.mock'; import PaymentContext, { PaymentContextProps } from '../PaymentContext'; +import { + CardInstrumentFieldset, + getInstrumentValidationSchema, + isInstrumentFeatureAvailable, +} from '../storedInstrument'; +import { getCardInstrument, getInstruments } from '../storedInstrument/instruments.mock'; -import CreditCardPaymentMethod, { CreditCardPaymentMethodProps, CreditCardPaymentMethodValues } from './CreditCardPaymentMethod'; +import CreditCardPaymentMethod, { + CreditCardPaymentMethodProps, + CreditCardPaymentMethodValues, +} from './CreditCardPaymentMethod'; jest.mock('../storedInstrument', () => ({ ...jest.requireActual('../storedInstrument'), - isInstrumentFeatureAvailable: jest.fn, Parameters>( - () => true - ), + isInstrumentFeatureAvailable: jest.fn< + ReturnType, + Parameters + >(() => true), })); describe('CreditCardPaymentMethod', () => { @@ -71,49 +83,37 @@ describe('CreditCardPaymentMethod', () => { }; subscribeEventEmitter = new EventEmitter(); - jest.spyOn(checkoutService, 'getState') - .mockReturnValue(checkoutState); + jest.spyOn(checkoutService, 'getState').mockReturnValue(checkoutState); - jest.spyOn(checkoutService, 'subscribe') - .mockImplementation(subscriber => { - subscribeEventEmitter.on('change', () => subscriber(checkoutState)); - subscribeEventEmitter.emit('change'); + jest.spyOn(checkoutService, 'subscribe').mockImplementation((subscriber) => { + subscribeEventEmitter.on('change', () => subscriber(checkoutState)); + subscribeEventEmitter.emit('change'); - return noop; - }); + return noop; + }); - jest.spyOn(checkoutService, 'loadInstruments') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'loadInstruments').mockResolvedValue(checkoutState); - jest.spyOn(checkoutState.data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(checkoutState.data, 'getCart').mockReturnValue(getCart()); - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - jest.spyOn(checkoutState.data, 'getConsignments') - .mockReturnValue([getConsignment()]); + jest.spyOn(checkoutState.data, 'getConsignments').mockReturnValue([getConsignment()]); - jest.spyOn(checkoutState.data, 'getCustomer') - .mockReturnValue(getCustomer()); + jest.spyOn(checkoutState.data, 'getCustomer').mockReturnValue(getCustomer()); - jest.spyOn(checkoutState.data, 'getInstruments') - .mockReturnValue([]); + jest.spyOn(checkoutState.data, 'getInstruments').mockReturnValue([]); - jest.spyOn(checkoutState.data, 'isPaymentDataRequired') - .mockReturnValue(true); + jest.spyOn(checkoutState.data, 'isPaymentDataRequired').mockReturnValue(true); - CreditCardPaymentMethodTest = props => { + CreditCardPaymentMethodTest = (props) => { return ( - - - - - - + + + + + + @@ -124,33 +124,35 @@ describe('CreditCardPaymentMethod', () => { }); it('initializes payment method when component mounts', async () => { - jest.spyOn(checkoutState.data, 'getInstruments') - .mockReturnValue(getInstruments()); + jest.spyOn(checkoutState.data, 'getInstruments').mockReturnValue(getInstruments()); - mount(); + mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(defaultProps.initializePayment) - .toHaveBeenCalledWith({ + expect(defaultProps.initializePayment).toHaveBeenCalledWith( + { methodId: defaultProps.method.id, - }, getInstruments()[0]); + }, + getInstruments()[0], + ); }); it('sets validation schema for credit cards when component mounts', () => { - mount(); + mount(); - expect(paymentContext.setValidationSchema) - .toHaveBeenCalled(); + expect(paymentContext.setValidationSchema).toHaveBeenCalled(); - const schema: Schema = (paymentContext.setValidationSchema as jest.Mock).mock.calls[0][1]; + const schema: Schema = (paymentContext.setValidationSchema as jest.Mock).mock + .calls[0][1]; const expectedSchema = getCreditCardValidationSchema({ isCardCodeRequired: true, language: localeContext.language, }); - expect(Object.keys(schema.describe().fields)) - .toEqual(Object.keys(expectedSchema.describe().fields)); + expect(Object.keys(schema.describe().fields)).toEqual( + Object.keys(expectedSchema.describe().fields), + ); }); it('uses custom validation schema if passed', () => { @@ -161,111 +163,96 @@ describe('CreditCardPaymentMethod', () => { ccNumber: string(), }); - mount(); + mount( + , + ); - expect(paymentContext.setValidationSchema) - .toHaveBeenCalled(); + expect(paymentContext.setValidationSchema).toHaveBeenCalled(); - const schema: Schema = (paymentContext.setValidationSchema as jest.Mock).mock.calls[0][1]; + const schema: Schema = (paymentContext.setValidationSchema as jest.Mock).mock + .calls[0][1]; - expect(Object.keys(schema.describe().fields)) - .toEqual(Object.keys(expectedSchema.describe().fields)); + expect(Object.keys(schema.describe().fields)).toEqual( + Object.keys(expectedSchema.describe().fields), + ); }); it('does not set validation schema if payment is not required', () => { - jest.spyOn(checkoutState.data, 'isPaymentDataRequired') - .mockReturnValue(false); + jest.spyOn(checkoutState.data, 'isPaymentDataRequired').mockReturnValue(false); - mount(); + mount(); - expect(paymentContext.setValidationSchema) - .toHaveBeenCalledWith(defaultProps.method, null); + expect(paymentContext.setValidationSchema).toHaveBeenCalledWith(defaultProps.method, null); }); it('deinitializes payment method when component unmounts', () => { - const component = mount(); + const component = mount(); - expect(defaultProps.deinitializePayment) - .not.toHaveBeenCalled(); + expect(defaultProps.deinitializePayment).not.toHaveBeenCalled(); component.unmount(); - expect(defaultProps.deinitializePayment) - .toHaveBeenCalled(); + expect(defaultProps.deinitializePayment).toHaveBeenCalled(); }); it('renders loading overlay while waiting for method to initialize', () => { let component: ReactWrapper; - component = mount(); + component = mount(); - expect(component.find(LoadingOverlay).prop('isLoading')) - .toEqual(true); + expect(component.find(LoadingOverlay).prop('isLoading')).toBe(true); - component = mount(); + component = mount(); - expect(component.find(LoadingOverlay).prop('isLoading')) - .toEqual(false); + expect(component.find(LoadingOverlay).prop('isLoading')).toBe(false); }); it('renders credit card fieldset', () => { - const component = mount(); + const component = mount(); - expect(component.find(CreditCardFieldset)) - .toHaveLength(1); + expect(component.find(CreditCardFieldset)).toHaveLength(1); }); it('renders custom credit card fieldset if passed', () => { const FoobarFieldset = () =>
    ; - const component = mount( } - />); + const component = mount( + } />, + ); - expect(component.find(FoobarFieldset)) - .toHaveLength(1); + expect(component.find(FoobarFieldset)).toHaveLength(1); }); describe('if stored instrument feature is available', () => { beforeEach(() => { - jest.spyOn(checkoutState.data, 'getInstruments') - .mockReturnValue(getInstruments()); + jest.spyOn(checkoutState.data, 'getInstruments').mockReturnValue(getInstruments()); - jest.spyOn(checkoutService, 'initializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'initializePayment').mockResolvedValue(checkoutState); - jest.spyOn(checkoutService, 'loadInstruments') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'loadInstruments').mockResolvedValue(checkoutState); }); it('loads stored instruments when component mounts', async () => { - mount(); + mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(checkoutService.loadInstruments) - .toHaveBeenCalled(); + expect(checkoutService.loadInstruments).toHaveBeenCalled(); }); it('sets validation schema for stored instruments when component mounts', () => { - jest.spyOn(checkoutState.data, 'getInstruments') - .mockReturnValue([{ + jest.spyOn(checkoutState.data, 'getInstruments').mockReturnValue([ + { ...getCardInstrument(), trustedShippingAddress: false, - }]); + }, + ]); - mount(); + mount(); - expect(paymentContext.setValidationSchema) - .toHaveBeenCalled(); + expect(paymentContext.setValidationSchema).toHaveBeenCalled(); - const schema: Schema = (paymentContext.setValidationSchema as jest.Mock).mock.calls[0][1]; + const schema: Schema = (paymentContext.setValidationSchema as jest.Mock).mock + .calls[0][1]; const expectedSchema = getInstrumentValidationSchema({ instrumentBrand: 'american_express', instrumentLast4: '4444', @@ -274,67 +261,57 @@ describe('CreditCardPaymentMethod', () => { language: localeContext.language, }); - expect(Object.keys(schema.describe().fields)) - .toEqual(Object.keys(expectedSchema.describe().fields)); + expect(Object.keys(schema.describe().fields)).toEqual( + Object.keys(expectedSchema.describe().fields), + ); }); it('only shows instruments fieldset when there is at least one stored instrument', () => { - const component = mount(); + const component = mount(); - expect(component.find(CardInstrumentFieldset)) - .toHaveLength(1); + expect(component.find(CardInstrumentFieldset)).toHaveLength(1); }); it('does not show instruments fieldset when there are no stored instruments', () => { - jest.spyOn(checkoutState.data, 'getInstruments') - .mockReturnValue([]); + jest.spyOn(checkoutState.data, 'getInstruments').mockReturnValue([]); - const component = mount(); + const component = mount(); - expect(component.find(CardInstrumentFieldset)) - .toHaveLength(0); + expect(component.find(CardInstrumentFieldset)).toHaveLength(0); }); it('shows save credit card form when there are no stored instruments', () => { - jest.spyOn(checkoutState.data, 'getInstruments') - .mockReturnValue([]); + jest.spyOn(checkoutState.data, 'getInstruments').mockReturnValue([]); - const container = mount(); + const container = mount(); - expect(container.find('input[name="shouldSaveInstrument"]').exists()) - .toBe(true); + expect(container.find('input[name="shouldSaveInstrument"]').exists()).toBe(true); }); it('uses PaymentMethod to retrieve instruments', () => { - mount(); + mount(); - expect(checkoutState.data.getInstruments) - .toHaveBeenCalledWith(defaultProps.method); + expect(checkoutState.data.getInstruments).toHaveBeenCalledWith(defaultProps.method); }); it('hides credit card fieldset if user is not adding new card', () => { - const component = mount(); + const component = mount(); - expect(component.find(CreditCardFieldset)) - .toHaveLength(0); + expect(component.find(CreditCardFieldset)).toHaveLength(0); - component.find(CardInstrumentFieldset) - .prop('onUseNewInstrument')(); + component.find(CardInstrumentFieldset).prop('onUseNewInstrument')(); component.update(); - expect(component.find(CardInstrumentFieldset).prop('selectedInstrumentId')) - .toEqual(undefined); + expect(component.find(CardInstrumentFieldset).prop('selectedInstrumentId')).toBeUndefined(); - expect(component.find(CreditCardFieldset)) - .toHaveLength(1); + expect(component.find(CreditCardFieldset)).toHaveLength(1); }); it('switches to "use new card" view if all instruments are deleted', () => { - const component = mount(); + const component = mount(); - expect(component.find(CreditCardFieldset)) - .toHaveLength(0); + expect(component.find(CreditCardFieldset)).toHaveLength(0); // Update state checkoutState = merge({}, checkoutState, { @@ -346,13 +323,13 @@ describe('CreditCardPaymentMethod', () => { subscribeEventEmitter.emit('change'); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - component.find(CardInstrumentFieldset) - .prop('onDeleteInstrument')!(getInstruments()[0].bigpayToken); + component.find(CardInstrumentFieldset).prop('onDeleteInstrument')!( + getInstruments()[0].bigpayToken, + ); component.update(); - expect(component.find(CreditCardFieldset)) - .toHaveLength(1); + expect(component.find(CreditCardFieldset)).toHaveLength(1); }); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/CreditCardPaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/CreditCardPaymentMethod.tsx index be18046deb..9cba3d6517 100644 --- a/packages/core/src/app/payment/paymentMethod/CreditCardPaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/CreditCardPaymentMethod.tsx @@ -1,21 +1,45 @@ -import { PaymentFormValues } from "@bigcommerce/checkout/payment-integration-api"; -import { CardInstrument, CheckoutSelectors, HostedFieldType, Instrument, PaymentInitializeOptions, PaymentInstrument, PaymentMethod, PaymentRequestOptions } from '@bigcommerce/checkout-sdk'; +import { + CardInstrument, + CheckoutSelectors, + HostedFieldType, + Instrument, + PaymentInitializeOptions, + PaymentInstrument, + PaymentMethod, + PaymentRequestOptions, +} from '@bigcommerce/checkout-sdk'; import { memoizeOne } from '@bigcommerce/memoize'; import { find, noop } from 'lodash'; import React, { Component, ReactNode } from 'react'; import { ObjectSchema } from 'yup'; -import { withCheckout, CheckoutContextProps } from '../../checkout'; +import { PaymentFormValues } from '@bigcommerce/checkout/payment-integration-api'; + +import { CheckoutContextProps, withCheckout } from '../../checkout'; import { connectFormik, ConnectFormikProps } from '../../common/form'; import { MapToPropsFactory } from '../../common/hoc'; import { withLanguage, WithLanguageProps } from '../../locale'; import { withForm, WithFormProps } from '../../ui/form'; import { LoadingOverlay } from '../../ui/loading'; -import { configureCardValidator, getCreditCardValidationSchema, CreditCardFieldset } from '../creditCard'; -import { getInstrumentValidationSchema, isCardInstrument, isInstrumentCardCodeRequiredSelector, isInstrumentCardNumberRequiredSelector, isInstrumentFeatureAvailable, CardInstrumentFieldset, CreditCardValidation, CardInstrumentFieldsetValues } from '../storedInstrument'; -import withPayment, { WithPaymentProps } from '../withPayment'; +import { + configureCardValidator, + CreditCardFieldset, + getCreditCardValidationSchema, +} from '../creditCard'; +import { + CardInstrumentFieldset, + CardInstrumentFieldsetValues, + CreditCardValidation, + getInstrumentValidationSchema, + isCardInstrument, + isInstrumentCardCodeRequiredSelector, + isInstrumentCardNumberRequiredSelector, + isInstrumentFeatureAvailable, +} from '../storedInstrument'; import StoreInstrumentFieldset from '../StoreInstrumentFieldset'; -import CreditCardFieldsetValues from "./CreditCardFieldsetValues"; +import withPayment, { WithPaymentProps } from '../withPayment'; + +import CreditCardFieldsetValues from './CreditCardFieldsetValues'; export interface CreditCardPaymentMethodProps { isInitializing?: boolean; @@ -26,7 +50,10 @@ export interface CreditCardPaymentMethodProps { storedCardValidationSchema?: ObjectSchema; deinitializePayment(options: PaymentRequestOptions): Promise; getStoredCardValidationFieldset?(selectedInstrument?: CardInstrument): ReactNode; - initializePayment(options: PaymentInitializeOptions, selectedInstrument?: CardInstrument): Promise; + initializePayment( + options: PaymentInitializeOptions, + selectedInstrument?: CardInstrument, + ): Promise; onUnhandledError?(error: Error): void; } @@ -82,10 +109,13 @@ class CreditCardPaymentMethod extends Component< await loadInstruments(); } - await initializePayment({ - gatewayId: method.gateway, - methodId: method.id, - }, this.getSelectedInstrument()); + await initializePayment( + { + gatewayId: method.gateway, + methodId: method.id, + }, + this.getSelectedInstrument(), + ); } catch (error) { onUnhandledError(error); } @@ -111,7 +141,10 @@ class CreditCardPaymentMethod extends Component< } } - async componentDidUpdate(_prevProps: Readonly, prevState: Readonly): Promise { + async componentDidUpdate( + _prevProps: Readonly, + prevState: Readonly, + ): Promise { const { deinitializePayment, initializePayment, @@ -120,25 +153,27 @@ class CreditCardPaymentMethod extends Component< setValidationSchema, } = this.props; - const { - isAddingNewCard, - selectedInstrumentId, - } = this.state; + const { isAddingNewCard, selectedInstrumentId } = this.state; setValidationSchema(method, this.getValidationSchema()); - if (selectedInstrumentId !== prevState.selectedInstrumentId || - isAddingNewCard !== prevState.isAddingNewCard) { + if ( + selectedInstrumentId !== prevState.selectedInstrumentId || + isAddingNewCard !== prevState.isAddingNewCard + ) { try { await deinitializePayment({ gatewayId: method.gateway, methodId: method.id, }); - await initializePayment({ - gatewayId: method.gateway, - methodId: method.id, - }, this.getSelectedInstrument()); + await initializePayment( + { + gatewayId: method.gateway, + methodId: method.id, + }, + this.getSelectedInstrument(), + ); } catch (error) { onUnhandledError(error); } @@ -164,39 +199,54 @@ class CreditCardPaymentMethod extends Component< const selectedInstrument = this.getSelectedInstrument(); const shouldShowCreditCardFieldset = !shouldShowInstrumentFieldset || isAddingNewCard; const isLoading = isInitializing || isLoadingInstruments; - const shouldShowNumberField = selectedInstrument ? isInstrumentCardNumberRequiredProp(selectedInstrument) : false; - const shouldShowCardCodeField = selectedInstrument ? isInstrumentCardCodeRequiredProp(selectedInstrument, method) : false; + const shouldShowNumberField = selectedInstrument + ? isInstrumentCardNumberRequiredProp(selectedInstrument) + : false; + const shouldShowCardCodeField = selectedInstrument + ? isInstrumentCardCodeRequiredProp(selectedInstrument, method) + : false; return ( - +
    - { shouldShowInstrumentFieldset && } - /> } - - { shouldShowCreditCardFieldset && !cardFieldset && } - - { shouldShowCreditCardFieldset && cardFieldset } - - { isInstrumentFeatureAvailableProp && } + {shouldShowInstrumentFieldset && ( + + ) + } + /> + )} + + {shouldShowCreditCardFieldset && !cardFieldset && ( + + )} + + {shouldShowCreditCardFieldset && cardFieldset} + + {isInstrumentFeatureAvailableProp && ( + + )}
    ); @@ -217,10 +267,8 @@ class CreditCardPaymentMethod extends Component< } const { instruments } = this.props; - const defaultInstrument = ( - instruments.find(instrument => instrument.defaultInstrument) || - instruments[0] - ); + const defaultInstrument = + instruments.find((instrument) => instrument.defaultInstrument) || instruments[0]; return defaultInstrument && defaultInstrument.bigpayToken; } @@ -244,19 +292,28 @@ class CreditCardPaymentMethod extends Component< const selectedInstrument = this.getSelectedInstrument(); if (isInstrumentFeatureAvailableProp && selectedInstrument) { - return storedCardValidationSchema || getInstrumentValidationSchema({ - instrumentBrand: selectedInstrument.brand, - instrumentLast4: selectedInstrument.last4, - isCardCodeRequired: isInstrumentCardCodeRequiredProp(selectedInstrument, method), - isCardNumberRequired: isInstrumentCardNumberRequiredProp(selectedInstrument), - language, - }); + return ( + storedCardValidationSchema || + getInstrumentValidationSchema({ + instrumentBrand: selectedInstrument.brand, + instrumentLast4: selectedInstrument.last4, + isCardCodeRequired: isInstrumentCardCodeRequiredProp( + selectedInstrument, + method, + ), + isCardNumberRequired: isInstrumentCardNumberRequiredProp(selectedInstrument), + language, + }) + ); } - return cardValidationSchema || getCreditCardValidationSchema({ - isCardCodeRequired: method.config.cardCode === true, - language, - }); + return ( + cardValidationSchema || + getCreditCardValidationSchema({ + isCardCodeRequired: method.config.cardCode === true, + language, + }) + ); } private handleUseNewCard: () => void = () => { @@ -266,15 +323,18 @@ class CreditCardPaymentMethod extends Component< }); }; - private handleSelectInstrument: (id: string) => void = id => { + private handleSelectInstrument: (id: string) => void = (id) => { this.setState({ isAddingNewCard: false, selectedInstrumentId: id, }); }; - private handleDeleteInstrument: (id: string) => void = id => { - const { instruments, formik: { setFieldValue } } = this.props; + private handleDeleteInstrument: (id: string) => void = (id) => { + const { + instruments, + formik: { setFieldValue }, + } = this.props; const { selectedInstrumentId } = this.state; if (instruments.length === 0) { @@ -299,26 +359,18 @@ const mapFromCheckoutProps: MapToPropsFactory< WithCheckoutCreditCardPaymentMethodProps, CreditCardPaymentMethodProps & ConnectFormikProps > = () => { - const filterInstruments = memoizeOne((instruments: PaymentInstrument[] = []) => instruments.filter(isCardInstrument)); + const filterInstruments = memoizeOne((instruments: PaymentInstrument[] = []) => + instruments.filter(isCardInstrument), + ); return (context, props) => { - const { - isUsingMultiShipping = false, - method, - } = props; + const { isUsingMultiShipping = false, method } = props; const { checkoutService, checkoutState } = context; const { - data: { - getConfig, - getCustomer, - getInstruments, - isPaymentDataRequired, - }, - statuses: { - isLoadingInstruments, - }, + data: { getConfig, getCustomer, getInstruments, isPaymentDataRequired }, + statuses: { isLoadingInstruments }, } = checkoutState; const config = getConfig(); @@ -346,9 +398,14 @@ const mapFromCheckoutProps: MapToPropsFactory< isLoadingInstruments: isLoadingInstruments(), isPaymentDataRequired: isPaymentDataRequired(), loadInstruments: checkoutService.loadInstruments, - shouldShowInstrumentFieldset: isInstrumentFeatureAvailableProp && instruments.length > 0, + shouldShowInstrumentFieldset: + isInstrumentFeatureAvailableProp && instruments.length > 0, }; }; }; -export default connectFormik(withForm(withLanguage(withPayment(withCheckout(mapFromCheckoutProps)(CreditCardPaymentMethod))))); +export default connectFormik( + withForm( + withLanguage(withPayment(withCheckout(mapFromCheckoutProps)(CreditCardPaymentMethod))), + ), +); diff --git a/packages/core/src/app/payment/paymentMethod/DigitalRiverPaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/DigitalRiverPaymentMethod.spec.tsx index d40296607e..28a25ab3ef 100644 --- a/packages/core/src/app/payment/paymentMethod/DigitalRiverPaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/DigitalRiverPaymentMethod.spec.tsx @@ -1,4 +1,9 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService, PaymentMethod } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, + PaymentMethod, +} from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import { Formik } from 'formik'; import { noop } from 'lodash'; @@ -9,7 +14,9 @@ import { getStoreConfig } from '../../config/config.mock'; import { createLocaleContext, LocaleContext, LocaleContextType } from '../../locale'; import { getPaymentMethod } from '../payment-methods.mock'; -import HostedDropInPaymentMethod, { HostedDropInPaymentMethodProps } from './HostedDropInPaymentMethod'; +import HostedDropInPaymentMethod, { + HostedDropInPaymentMethodProps, +} from './HostedDropInPaymentMethod'; import { default as PaymentMethodComponent, PaymentMethodProps } from './PaymentMethod'; import PaymentMethodId from './PaymentMethodId'; @@ -21,6 +28,7 @@ describe('when using Digital River payment', () => { let localeContext: LocaleContextType; let PaymentMethodTest: FunctionComponent; const digitalRiverMethod = getPaymentMethod(); + digitalRiverMethod.config.isVaultingEnabled = true; beforeEach(() => { @@ -33,23 +41,17 @@ describe('when using Digital River payment', () => { localeContext = createLocaleContext(getStoreConfig()); method = { ...digitalRiverMethod, id: PaymentMethodId.DigitalRiver }; - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - jest.spyOn(checkoutService, 'deinitializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'deinitializePayment').mockResolvedValue(checkoutState); - jest.spyOn(checkoutService, 'initializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'initializePayment').mockResolvedValue(checkoutState); - PaymentMethodTest = props => ( - - - - + PaymentMethodTest = (props) => ( + + + + @@ -57,87 +59,89 @@ describe('when using Digital River payment', () => { }); it('renders as hosted drop in method', () => { - const container = mount(); - const component: ReactWrapper = container.find(HostedDropInPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(HostedDropInPaymentMethod); - expect(component.props()) - .toEqual(expect.objectContaining({ + expect(component.props()).toEqual( + expect.objectContaining({ containerId: `${method.id}-component-field`, initializePayment: expect.any(Function), method, onUnhandledError: expect.any(Function), - })); + }), + ); }); it('initializes method with required config including initializationData', () => { - const container = mount(); - const component: ReactWrapper = container.find(HostedDropInPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(HostedDropInPaymentMethod); component.prop('initializePayment')({ methodId: method.id, gatewayId: method.gateway, }); - expect(checkoutService.initializePayment) - .toHaveBeenCalledWith({ - digitalriver: { - configuration: { - button: { - type: 'submitOrder', - }, - flow: 'checkout', - paymentMethodConfiguration: { - classes: { - base: 'form-input optimizedCheckout-form-input', - }, + expect(checkoutService.initializePayment).toHaveBeenCalledWith({ + digitalriver: { + configuration: { + button: { + type: 'submitOrder', + }, + flow: 'checkout', + paymentMethodConfiguration: { + classes: { + base: 'form-input optimizedCheckout-form-input', }, - showComplianceSection: false, - showSavePaymentAgreement: true, - showTermsOfSaleDisclosure: true, - usage: 'unscheduled', }, - containerId: `${method.id}-component-field`, - onError: expect.any(Function), - onSubmitForm: expect.any(Function), + showComplianceSection: false, + showSavePaymentAgreement: true, + showTermsOfSaleDisclosure: true, + usage: 'unscheduled', }, - gatewayId: undefined, - methodId: 'digitalriver', - }); + containerId: `${method.id}-component-field`, + onError: expect.any(Function), + onSubmitForm: expect.any(Function), + }, + gatewayId: undefined, + methodId: 'digitalriver', + }); }); it('initializes method with required config and without initializationData', () => { - const container = mount(); - const component: ReactWrapper = container.find(HostedDropInPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(HostedDropInPaymentMethod); component.prop('initializePayment')({ methodId: method.id, gatewayId: method.gateway, }); - expect(checkoutService.initializePayment) - .toHaveBeenCalledWith({ - digitalriver: { - configuration: { - button: { - type: 'submitOrder', - }, - flow: 'checkout', - paymentMethodConfiguration: { - classes: { - base: 'form-input optimizedCheckout-form-input', - }, + expect(checkoutService.initializePayment).toHaveBeenCalledWith({ + digitalriver: { + configuration: { + button: { + type: 'submitOrder', + }, + flow: 'checkout', + paymentMethodConfiguration: { + classes: { + base: 'form-input optimizedCheckout-form-input', }, - showComplianceSection: false, - showSavePaymentAgreement: true, - showTermsOfSaleDisclosure: true, - usage: 'unscheduled', }, - containerId: `${method.id}-component-field`, - onError: expect.any(Function), - onSubmitForm: expect.any(Function), + showComplianceSection: false, + showSavePaymentAgreement: true, + showTermsOfSaleDisclosure: true, + usage: 'unscheduled', }, - gatewayId: undefined, - methodId: 'digitalriver', - }); + containerId: `${method.id}-component-field`, + onError: expect.any(Function), + onSubmitForm: expect.any(Function), + }, + gatewayId: undefined, + methodId: 'digitalriver', + }); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/DigitalRiverPaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/DigitalRiverPaymentMethod.tsx index 870caa7851..ecdff18cc0 100644 --- a/packages/core/src/app/payment/paymentMethod/DigitalRiverPaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/DigitalRiverPaymentMethod.tsx @@ -1,8 +1,9 @@ -import { PaymentFormValues } from '@bigcommerce/checkout/payment-integration-api'; import { noop } from 'lodash'; -import React, { useCallback, useContext, FunctionComponent } from 'react'; +import React, { FunctionComponent, useCallback, useContext } from 'react'; import { Omit } from 'utility-types'; +import { PaymentFormValues } from '@bigcommerce/checkout/payment-integration-api'; + import { CustomError } from '../../common/error'; import { connectFormik, ConnectFormikProps } from '../../common/form'; import { withLanguage, WithLanguageProps } from '../../locale'; @@ -11,49 +12,60 @@ import { FormContext } from '../../ui/form'; import HostedDropInPaymentMethod from './HostedDropInPaymentMethod'; import { HostedWidgetPaymentMethodProps } from './HostedWidgetPaymentMethod'; -export type DigitalRiverPaymentMethodProps = Omit & ConnectFormikProps; +export type DigitalRiverPaymentMethodProps = Omit & + ConnectFormikProps; export enum DigitalRiverClasses { - base = 'form-input optimizedCheckout-form-input', + base = 'form-input optimizedCheckout-form-input', } -const DigitalRiverPaymentMethod: FunctionComponent = ({ - initializePayment, - language, - onUnhandledError = noop, - formik: { submitForm }, - ...rest -}) => { +const DigitalRiverPaymentMethod: FunctionComponent< + DigitalRiverPaymentMethodProps & WithLanguageProps +> = ({ initializePayment, language, onUnhandledError = noop, formik: { submitForm }, ...rest }) => { const { setSubmitted } = useContext(FormContext); const containerId = `${rest.method.id}-component-field`; const isVaultingEnabled = rest.method.config.isVaultingEnabled; - const initializeDigitalRiverPayment = useCallback(options => initializePayment({ - ...options, - digitalriver: { - containerId, - configuration: { - flow: 'checkout', - showSavePaymentAgreement: isVaultingEnabled, - showComplianceSection: false, - button: { - type: 'submitOrder', - }, - usage: 'unscheduled', - showTermsOfSaleDisclosure: true, - paymentMethodConfiguration: { - classes: DigitalRiverClasses, + const initializeDigitalRiverPayment = useCallback( + (options) => + initializePayment({ + ...options, + digitalriver: { + containerId, + configuration: { + flow: 'checkout', + showSavePaymentAgreement: isVaultingEnabled, + showComplianceSection: false, + button: { + type: 'submitOrder', + }, + usage: 'unscheduled', + showTermsOfSaleDisclosure: true, + paymentMethodConfiguration: { + classes: DigitalRiverClasses, + }, + }, + onSubmitForm: () => { + setSubmitted(true); + submitForm(); + }, + onError: () => { + onUnhandledError( + new Error(language.translate('payment.digitalriver_dropin_error')), + ); + }, }, - }, - onSubmitForm: () => { - setSubmitted(true); - submitForm(); - }, - onError: () => { - onUnhandledError?.(new Error(language.translate('payment.digitalriver_dropin_error'))); - }, - }, - }), [initializePayment, containerId, isVaultingEnabled, setSubmitted, submitForm, onUnhandledError, language]); + }), + [ + initializePayment, + containerId, + isVaultingEnabled, + setSubmitted, + submitForm, + onUnhandledError, + language, + ], + ); const onError = (error: CustomError) => { if (error.name === 'digitalRiverCheckoutError') { @@ -65,16 +77,18 @@ const DigitalRiverPaymentMethod: FunctionComponent; + return ( + + ); }; export default connectFormik(withLanguage(DigitalRiverPaymentMethod)); diff --git a/packages/core/src/app/payment/paymentMethod/GooglePayPaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/GooglePayPaymentMethod.spec.tsx index 288bf97ff8..3f90a2a2c8 100644 --- a/packages/core/src/app/payment/paymentMethod/GooglePayPaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/GooglePayPaymentMethod.spec.tsx @@ -1,4 +1,10 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService, PaymentInitializeOptions, PaymentMethod } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, + PaymentInitializeOptions, + PaymentMethod, +} from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import { Formik } from 'formik'; import each from 'jest-each'; @@ -13,7 +19,9 @@ import { getPaymentMethod } from '../payment-methods.mock'; import { default as PaymentMethodComponent, PaymentMethodProps } from './PaymentMethod'; import PaymentMethodId from './PaymentMethodId'; import PaymentMethodType from './PaymentMethodType'; -import WalletButtonPaymentMethod, { WalletButtonPaymentMethodProps } from './WalletButtonPaymentMethod'; +import WalletButtonPaymentMethod, { + WalletButtonPaymentMethodProps, +} from './WalletButtonPaymentMethod'; describe('when using Google Pay payment', () => { let method: PaymentMethod; @@ -38,23 +46,17 @@ describe('when using Google Pay payment', () => { method: PaymentMethodType.GooglePay, }; - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - jest.spyOn(checkoutService, 'deinitializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'deinitializePayment').mockResolvedValue(checkoutState); - jest.spyOn(checkoutService, 'initializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'initializePayment').mockResolvedValue(checkoutState); - PaymentMethodTest = props => ( - - - - + PaymentMethodTest = (props) => ( + + + + @@ -62,16 +64,17 @@ describe('when using Google Pay payment', () => { }); it('renders as wallet button method', () => { - const container = mount(); + const container = mount(); - expect(container.find(WalletButtonPaymentMethod).props()) - .toEqual(expect.objectContaining({ + expect(container.find(WalletButtonPaymentMethod).props()).toEqual( + expect.objectContaining({ buttonId: 'walletButton', deinitializePayment: expect.any(Function), initializePayment: expect.any(Function), method, shouldShowEditButton: true, - })); + }), + ); }); each([ @@ -84,18 +87,20 @@ describe('when using Google Pay payment', () => { [PaymentMethodId.OrbitalGooglePay], [PaymentMethodId.StripeGooglePay], [PaymentMethodId.StripeUPEGooglePay], - ]).it('initializes %s with required config', id => { + ]).it('initializes %s with required config', (id) => { method.id = id; - const container = mount(); - const component: ReactWrapper = container.find(WalletButtonPaymentMethod); + + const container = mount(); + const component: ReactWrapper = + container.find(WalletButtonPaymentMethod); component.prop('initializePayment')({ methodId: method.id, gatewayId: method.gateway, }); - expect(checkoutService.initializePayment) - .toHaveBeenCalledWith(expect.objectContaining({ + expect(checkoutService.initializePayment).toHaveBeenCalledWith( + expect.objectContaining({ methodId: id, gatewayId: method.gateway, [id]: { @@ -103,35 +108,38 @@ describe('when using Google Pay payment', () => { onError: defaultProps.onUnhandledError, onPaymentSelect: expect.any(Function), }, - })); + }), + ); }); it('reinitializes method once payment option is selected', async () => { - const container = mount(); - const component: ReactWrapper = container.find(WalletButtonPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(WalletButtonPaymentMethod); component.prop('initializePayment')({ methodId: method.id, gatewayId: method.gateway, }); - const options: PaymentInitializeOptions = (checkoutService.initializePayment as jest.Mock).mock.calls[0][0]; + const options: PaymentInitializeOptions = (checkoutService.initializePayment as jest.Mock) + .mock.calls[0][0]; const paymentSelectHandler = options.googlepaybraintree?.onPaymentSelect; + if (paymentSelectHandler) { paymentSelectHandler(); } - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(checkoutService.deinitializePayment) - .toHaveBeenCalledWith({ methodId: method.id }); - expect(checkoutService.initializePayment) - .toHaveBeenCalledWith(expect.objectContaining({ + expect(checkoutService.deinitializePayment).toHaveBeenCalledWith({ methodId: method.id }); + expect(checkoutService.initializePayment).toHaveBeenCalledWith( + expect.objectContaining({ methodId: method.id, [method.id]: expect.any(Object), - })); - expect(checkoutService.initializePayment) - .toHaveBeenCalledTimes(2); + }), + ); + expect(checkoutService.initializePayment).toHaveBeenCalledTimes(2); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/GooglePayPaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/GooglePayPaymentMethod.tsx index fd3e17a399..f983105438 100644 --- a/packages/core/src/app/payment/paymentMethod/GooglePayPaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/GooglePayPaymentMethod.tsx @@ -1,109 +1,107 @@ import { PaymentInitializeOptions } from '@bigcommerce/checkout-sdk'; import { noop } from 'lodash'; -import React, { useCallback, FunctionComponent } from 'react'; +import React, { FunctionComponent, useCallback } from 'react'; import { Omit } from 'utility-types'; -import WalletButtonPaymentMethod, { WalletButtonPaymentMethodProps } from './WalletButtonPaymentMethod'; +import WalletButtonPaymentMethod, { + WalletButtonPaymentMethodProps, +} from './WalletButtonPaymentMethod'; export type GooglePayPaymentMethodProps = Omit< - WalletButtonPaymentMethodProps, - 'buttonId' | 'shouldShowEditButton' + WalletButtonPaymentMethodProps, + 'buttonId' | 'shouldShowEditButton' >; -const GooglePayPaymentMethod: FunctionComponent = - ({ +const GooglePayPaymentMethod: FunctionComponent = ({ deinitializePayment, initializePayment, method, onUnhandledError = noop, ...rest - }) => { - const initializeGooglePayPayment = useCallback((defaultOptions: PaymentInitializeOptions) => { - const reinitializePayment = async (options: PaymentInitializeOptions) => { - try { - await deinitializePayment({ - gatewayId: method.gateway, - methodId: method.id, - }); +}) => { + const initializeGooglePayPayment = useCallback( + (defaultOptions: PaymentInitializeOptions) => { + const reinitializePayment = async (options: PaymentInitializeOptions) => { + try { + await deinitializePayment({ + gatewayId: method.gateway, + methodId: method.id, + }); - await initializePayment({ - ...options, - gatewayId: method.gateway, - methodId: method.id, - }); - } catch (error) { - onUnhandledError(error); - } - }; + await initializePayment({ + ...options, + gatewayId: method.gateway, + methodId: method.id, + }); + } catch (error) { + onUnhandledError(error); + } + }; - const mergedOptions = { - ...defaultOptions, - googlepayadyenv2: { - walletButton: 'walletButton', - onError: onUnhandledError, - onPaymentSelect: () => reinitializePayment(mergedOptions), - }, - googlepayadyenv3: { - walletButton: 'walletButton', - onError: onUnhandledError, - onPaymentSelect: () => reinitializePayment(mergedOptions), - }, - googlepayauthorizenet: { - walletButton: 'walletButton', - onError: onUnhandledError, - onPaymentSelect: () => reinitializePayment(mergedOptions), - }, - googlepaybraintree: { - walletButton: 'walletButton', - onError: onUnhandledError, - onPaymentSelect: () => reinitializePayment(mergedOptions), - }, - googlepaystripe: { - walletButton: 'walletButton', - onError: onUnhandledError, - onPaymentSelect: () => reinitializePayment(mergedOptions), - }, - googlepaystripeupe: { - walletButton: 'walletButton', - onError: onUnhandledError, - onPaymentSelect: () => reinitializePayment(mergedOptions), - }, - googlepaycybersourcev2: { - walletButton: 'walletButton', - onError: onUnhandledError, - onPaymentSelect: () => reinitializePayment(mergedOptions), - }, - googlepayorbital: { - walletButton: 'walletButton', - onError: onUnhandledError, - onPaymentSelect: () => reinitializePayment(mergedOptions), - }, - googlepaycheckoutcom: { - walletButton: 'walletButton', - onError: onUnhandledError, - onPaymentSelect: () => reinitializePayment(mergedOptions), - }, - }; + const mergedOptions = { + ...defaultOptions, + googlepayadyenv2: { + walletButton: 'walletButton', + onError: onUnhandledError, + onPaymentSelect: () => reinitializePayment(mergedOptions), + }, + googlepayadyenv3: { + walletButton: 'walletButton', + onError: onUnhandledError, + onPaymentSelect: () => reinitializePayment(mergedOptions), + }, + googlepayauthorizenet: { + walletButton: 'walletButton', + onError: onUnhandledError, + onPaymentSelect: () => reinitializePayment(mergedOptions), + }, + googlepaybraintree: { + walletButton: 'walletButton', + onError: onUnhandledError, + onPaymentSelect: () => reinitializePayment(mergedOptions), + }, + googlepaystripe: { + walletButton: 'walletButton', + onError: onUnhandledError, + onPaymentSelect: () => reinitializePayment(mergedOptions), + }, + googlepaystripeupe: { + walletButton: 'walletButton', + onError: onUnhandledError, + onPaymentSelect: () => reinitializePayment(mergedOptions), + }, + googlepaycybersourcev2: { + walletButton: 'walletButton', + onError: onUnhandledError, + onPaymentSelect: () => reinitializePayment(mergedOptions), + }, + googlepayorbital: { + walletButton: 'walletButton', + onError: onUnhandledError, + onPaymentSelect: () => reinitializePayment(mergedOptions), + }, + googlepaycheckoutcom: { + walletButton: 'walletButton', + onError: onUnhandledError, + onPaymentSelect: () => reinitializePayment(mergedOptions), + }, + }; - return initializePayment(mergedOptions); - }, - [ - deinitializePayment, - initializePayment, - method, - onUnhandledError, - ]); + return initializePayment(mergedOptions); + }, + [deinitializePayment, initializePayment, method, onUnhandledError], + ); return ( ); - }; +}; export default GooglePayPaymentMethod; diff --git a/packages/core/src/app/payment/paymentMethod/HostedCreditCardFieldsetValues.ts b/packages/core/src/app/payment/paymentMethod/HostedCreditCardFieldsetValues.ts index 72f7dba744..0eef136310 100644 --- a/packages/core/src/app/payment/paymentMethod/HostedCreditCardFieldsetValues.ts +++ b/packages/core/src/app/payment/paymentMethod/HostedCreditCardFieldsetValues.ts @@ -1,4 +1,4 @@ -import { CardInstrumentFieldsetValues } from "../storedInstrument"; +import { CardInstrumentFieldsetValues } from '../storedInstrument'; export default interface HostedCreditCardFieldsetValues { hostedForm: { @@ -21,11 +21,13 @@ export interface HostedCreditCardValidationValues extends CardInstrumentFieldset }; } -export function isHostedCreditCardFieldsetValues(value: unknown): value is HostedCreditCardFieldsetValues { +export function isHostedCreditCardFieldsetValues( + value: unknown, +): value is HostedCreditCardFieldsetValues { if (!(value instanceof Object)) { return false; } - + if (!('hostedForm' in value)) { return false; } diff --git a/packages/core/src/app/payment/paymentMethod/HostedCreditCardPaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/HostedCreditCardPaymentMethod.spec.tsx index c0381729b8..13c67c50f8 100644 --- a/packages/core/src/app/payment/paymentMethod/HostedCreditCardPaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/HostedCreditCardPaymentMethod.spec.tsx @@ -2,11 +2,16 @@ import { mount } from 'enzyme'; import React, { useEffect } from 'react'; import { object } from 'yup'; -import { withHostedCreditCardFieldset, WithInjectedHostedCreditCardFieldsetProps } from '../hostedCreditCard'; +import { + withHostedCreditCardFieldset, + WithInjectedHostedCreditCardFieldsetProps, +} from '../hostedCreditCard'; import { getPaymentMethod } from '../payment-methods.mock'; import CreditCardPaymentMethod from './CreditCardPaymentMethod'; -import HostedCreditCardPaymentMethod, { HostedCreditCardPaymentMethodProps } from './HostedCreditCardPaymentMethod'; +import HostedCreditCardPaymentMethod, { + HostedCreditCardPaymentMethodProps, +} from './HostedCreditCardPaymentMethod'; const hostedFormOptions = { fields: { @@ -27,28 +32,24 @@ const injectedProps: WithInjectedHostedCreditCardFieldsetProps = { jest.mock('../hostedCreditCard', () => ({ ...jest.requireActual('../hostedCreditCard'), - withHostedCreditCardFieldset: jest.fn( - Component => (props: any) => - ) as jest.Mocked, + withHostedCreditCardFieldset: jest.fn((Component) => (props: any) => ( + + )) as jest.Mocked, })); -jest.mock('./CreditCardPaymentMethod', () => - jest.fn(({ - initializePayment, - method, - }) => { - useEffect(() => { - initializePayment({ - methodId: method.id, - gatewayId: method.gateway, +jest.mock( + './CreditCardPaymentMethod', + () => + jest.fn(({ initializePayment, method }) => { + useEffect(() => { + initializePayment({ + methodId: method.id, + gatewayId: method.gateway, + }); }); - }); - return
    ; - }) as jest.Mocked + return
    ; + }) as jest.Mocked, ); describe('HostedCreditCardPaymentMethod', () => { @@ -63,33 +64,34 @@ describe('HostedCreditCardPaymentMethod', () => { }); it('initializes method with hosted form options', async () => { - mount(); + mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(defaultProps.initializePayment) - .toHaveBeenCalledWith({ - methodId: defaultProps.method.id, - creditCard: { - form: hostedFormOptions, - }, - }); + expect(defaultProps.initializePayment).toHaveBeenCalledWith({ + methodId: defaultProps.method.id, + creditCard: { + form: hostedFormOptions, + }, + }); }); it('injects hosted form properties to credit card payment method component', async () => { - const component = mount(); + const component = mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); const decoratedComponent = component.find(CreditCardPaymentMethod); - expect(decoratedComponent.prop('cardFieldset')) - .toEqual(injectedProps.hostedFieldset); - expect(decoratedComponent.prop('cardValidationSchema')) - .toEqual(injectedProps.hostedValidationSchema); - expect(decoratedComponent.prop('getStoredCardValidationFieldset')) - .toEqual(injectedProps.getHostedStoredCardValidationFieldset); - expect(decoratedComponent.prop('storedCardValidationSchema')) - .toEqual(injectedProps.hostedStoredCardValidationSchema); + expect(decoratedComponent.prop('cardFieldset')).toEqual(injectedProps.hostedFieldset); + expect(decoratedComponent.prop('cardValidationSchema')).toEqual( + injectedProps.hostedValidationSchema, + ); + expect(decoratedComponent.prop('getStoredCardValidationFieldset')).toEqual( + injectedProps.getHostedStoredCardValidationFieldset, + ); + expect(decoratedComponent.prop('storedCardValidationSchema')).toEqual( + injectedProps.hostedStoredCardValidationSchema, + ); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/HostedCreditCardPaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/HostedCreditCardPaymentMethod.tsx index b682fed60c..307e964dae 100644 --- a/packages/core/src/app/payment/paymentMethod/HostedCreditCardPaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/HostedCreditCardPaymentMethod.tsx @@ -1,20 +1,22 @@ -import React, { useCallback, FunctionComponent } from 'react'; +import React, { FunctionComponent, useCallback } from 'react'; -import { withHostedCreditCardFieldset, WithInjectedHostedCreditCardFieldsetProps } from '../hostedCreditCard'; +import { + withHostedCreditCardFieldset, + WithInjectedHostedCreditCardFieldsetProps, +} from '../hostedCreditCard'; import CreditCardPaymentMethod, { CreditCardPaymentMethodProps } from './CreditCardPaymentMethod'; export type HostedCreditCardPaymentMethodProps = Omit< CreditCardPaymentMethodProps, - 'cardFieldset' | - 'cardValidationSchema' | - 'storedCardValidationSchema' | - 'getStoredCardValidationFieldset' + | 'cardFieldset' + | 'cardValidationSchema' + | 'storedCardValidationSchema' + | 'getStoredCardValidationFieldset' >; const HostedCreditCardPaymentMethod: FunctionComponent< - HostedCreditCardPaymentMethodProps & - WithInjectedHostedCreditCardFieldsetProps + HostedCreditCardPaymentMethodProps & WithInjectedHostedCreditCardFieldsetProps > = ({ getHostedFormOptions, getHostedStoredCardValidationFieldset, @@ -24,26 +26,29 @@ const HostedCreditCardPaymentMethod: FunctionComponent< initializePayment, ...rest }) => { - const initializeHostedCreditCardPayment: CreditCardPaymentMethodProps['initializePayment'] = useCallback(async (options, selectedInstrument) => { - return initializePayment({ - ...options, - creditCard: getHostedFormOptions && { - form: await getHostedFormOptions(selectedInstrument), + const initializeHostedCreditCardPayment: CreditCardPaymentMethodProps['initializePayment'] = + useCallback( + async (options, selectedInstrument) => { + return initializePayment({ + ...options, + creditCard: getHostedFormOptions && { + form: await getHostedFormOptions(selectedInstrument), + }, + }); }, - }); - }, [ - getHostedFormOptions, - initializePayment, - ]); + [getHostedFormOptions, initializePayment], + ); - return ; + return ( + + ); }; export default withHostedCreditCardFieldset(HostedCreditCardPaymentMethod); diff --git a/packages/core/src/app/payment/paymentMethod/HostedDropInPaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/HostedDropInPaymentMethod.spec.tsx index cc16124bc9..428c36206b 100644 --- a/packages/core/src/app/payment/paymentMethod/HostedDropInPaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/HostedDropInPaymentMethod.spec.tsx @@ -1,4 +1,8 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, +} from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import { EventEmitter } from 'events'; import { Formik } from 'formik'; @@ -14,13 +18,15 @@ import { createLocaleContext, LocaleContext } from '../../locale'; import { getConsignment } from '../../shipping/consignment.mock'; import { LoadingOverlay } from '../../ui/loading'; import { getPaymentMethod } from '../payment-methods.mock'; +import PaymentContext, { PaymentContextProps } from '../PaymentContext'; import { CardInstrumentFieldset } from '../storedInstrument'; import { getInstruments } from '../storedInstrument/instruments.mock'; -import PaymentContext, { PaymentContextProps } from '../PaymentContext'; -import HostedDropInPaymentMethod, { HostedDropInPaymentMethodProps } from './HostedDropInPaymentMethod'; +import HostedDropInPaymentMethod, { + HostedDropInPaymentMethodProps, +} from './HostedDropInPaymentMethod'; -describe('HostedDropInPaymentMethod', () => { +describe('HostedDropInPaymentMethod', () => { let HostedDropInPaymentMethodTest: FunctionComponent; let defaultProps: HostedDropInPaymentMethodProps; let checkoutService: CheckoutService; @@ -48,41 +54,31 @@ describe('HostedDropInPaymentMethod', () => { }; subscribeEventEmitter = new EventEmitter(); - jest.spyOn(checkoutService, 'subscribe') - .mockImplementation(subscriber => { - subscribeEventEmitter.on('change', () => subscriber(checkoutState)); - subscribeEventEmitter.emit('change'); + jest.spyOn(checkoutService, 'subscribe').mockImplementation((subscriber) => { + subscribeEventEmitter.on('change', () => subscriber(checkoutState)); + subscribeEventEmitter.emit('change'); - return noop; - }); + return noop; + }); - jest.spyOn(checkoutState.data, 'getCheckout') - .mockReturnValue(getCheckout()); + jest.spyOn(checkoutState.data, 'getCheckout').mockReturnValue(getCheckout()); - jest.spyOn(checkoutState.data, 'isPaymentDataRequired') - .mockReturnValue(true); + jest.spyOn(checkoutState.data, 'isPaymentDataRequired').mockReturnValue(true); - jest.spyOn(checkoutState.data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(checkoutState.data, 'getCart').mockReturnValue(getCart()); - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - jest.spyOn(checkoutState.data, 'getConsignments') - .mockReturnValue([getConsignment()]); + jest.spyOn(checkoutState.data, 'getConsignments').mockReturnValue([getConsignment()]); - jest.spyOn(checkoutState.data, 'getCustomer') - .mockReturnValue(getCustomer()); + jest.spyOn(checkoutState.data, 'getCustomer').mockReturnValue(getCustomer()); - HostedDropInPaymentMethodTest = props => ( - - - - - + HostedDropInPaymentMethodTest = (props) => ( + + + + + @@ -91,179 +87,162 @@ describe('HostedDropInPaymentMethod', () => { }); it('initializes payment method when component mounts', () => { - mount(); + mount(); - expect(defaultProps.initializePayment) - .toHaveBeenCalled(); + expect(defaultProps.initializePayment).toHaveBeenCalled(); expect(paymentContext.setSubmit).toHaveBeenCalled(); }); it('deinitializes payment method when component unmounts', () => { - const component = mount(); + const component = mount(); - expect(defaultProps.deinitializePayment) - .not.toHaveBeenCalled(); + expect(defaultProps.deinitializePayment).not.toHaveBeenCalled(); component.unmount(); - expect(defaultProps.deinitializePayment) - .toHaveBeenCalled(); + expect(defaultProps.deinitializePayment).toHaveBeenCalled(); }); it('does not initialize payment method if payment data is not required', () => { - jest.spyOn(checkoutState.data, 'isPaymentDataRequired') - .mockReturnValue(false); + jest.spyOn(checkoutState.data, 'isPaymentDataRequired').mockReturnValue(false); - mount(); + mount(); - expect(defaultProps.initializePayment) - .not.toHaveBeenCalled(); + expect(defaultProps.initializePayment).not.toHaveBeenCalled(); }); it('renders loading overlay while waiting for method to initialize', () => { let component: ReactWrapper; - component = mount(); + component = mount(); - expect(component.find(LoadingOverlay).prop('isLoading')) - .toEqual(true); + expect(component.find(LoadingOverlay).prop('isLoading')).toBe(true); - component = mount(); + component = mount(); - expect(component.find(LoadingOverlay).prop('isLoading')) - .toEqual(false); + expect(component.find(LoadingOverlay).prop('isLoading')).toBe(false); }); it('hides content while loading', () => { - const component = mount(); + const component = mount(); - expect(component.find(LoadingOverlay).prop('hideContentWhenLoading')) - .toEqual(true); + expect(component.find(LoadingOverlay).prop('hideContentWhenLoading')).toBe(true); }); it('renders placeholder container with provided ID', () => { - const component = mount(); + const component = mount(); - expect(component.exists(`#${defaultProps.containerId}`)) - .toEqual(true); + expect(component.exists(`#${defaultProps.containerId}`)).toBe(true); }); it('deinitializes payment method when component unmounts', () => { - const component = mount(); + const component = mount(); - expect(defaultProps.deinitializePayment) - .not.toHaveBeenCalled(); + expect(defaultProps.deinitializePayment).not.toHaveBeenCalled(); component.unmount(); - expect(defaultProps.deinitializePayment) - .toHaveBeenCalled(); + expect(defaultProps.deinitializePayment).toHaveBeenCalled(); }); describe('if stored instrument feature is available', () => { beforeEach(() => { defaultProps.method.config.isVaultingEnabled = true; - jest.spyOn(checkoutState.data, 'getInstruments') - .mockReturnValue(getInstruments()); - jest.spyOn(checkoutService, 'loadInstruments') - .mockResolvedValue(checkoutState); - jest.spyOn(checkoutService, 'initializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutState.data, 'getInstruments').mockReturnValue(getInstruments()); + jest.spyOn(checkoutService, 'loadInstruments').mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'initializePayment').mockResolvedValue(checkoutState); }); it('loads stored instruments when component mounts', () => { - mount(); + mount(); - expect(checkoutService.loadInstruments) - .toHaveBeenCalled(); + expect(checkoutService.loadInstruments).toHaveBeenCalled(); }); it('only shows instruments fieldset when there is at least one stored instrument', () => { - const component = mount(); + const component = mount(); - expect(component.find(CardInstrumentFieldset)) - .toHaveLength(1); + expect(component.find(CardInstrumentFieldset)).toHaveLength(1); }); it('uses PaymentMethod to retrieve instruments', () => { - mount(); + mount(); - expect(checkoutState.data.getInstruments) - .toHaveBeenCalledWith(defaultProps.method); + expect(checkoutState.data.getInstruments).toHaveBeenCalledWith(defaultProps.method); }); it('shows the payment submit button when a vaulted instrument is selected', () => { - const component = mount(); + const component = mount(); - component.find(CardInstrumentFieldset) - .prop('onSelectInstrument')(getInstruments()[0].bigpayToken); + component.find(CardInstrumentFieldset).prop('onSelectInstrument')( + getInstruments()[0].bigpayToken, + ); component.update(); - expect(paymentContext.hidePaymentSubmitButton) - .toHaveBeenCalledWith(defaultProps.method, false); + expect(paymentContext.hidePaymentSubmitButton).toHaveBeenCalledWith( + defaultProps.method, + false, + ); }); it('shows the payment submit button when payment data is not required', () => { - jest.spyOn(checkoutState.data, 'isPaymentDataRequired') - .mockReturnValue(false); + jest.spyOn(checkoutState.data, 'isPaymentDataRequired').mockReturnValue(false); - const component = mount(); + const component = mount(); - component.find(CardInstrumentFieldset) - .prop('onUseNewInstrument')(); + component.find(CardInstrumentFieldset).prop('onUseNewInstrument')(); component.update(); - expect(paymentContext.hidePaymentSubmitButton) - .toHaveBeenCalledWith(defaultProps.method, false); + expect(paymentContext.hidePaymentSubmitButton).toHaveBeenCalledWith( + defaultProps.method, + false, + ); }); it('shows the payment submit button when payment data is required', () => { - jest.spyOn(checkoutState.data, 'isPaymentDataRequired') - .mockReturnValue(true); + jest.spyOn(checkoutState.data, 'isPaymentDataRequired').mockReturnValue(true); - const component = mount(); + const component = mount(); - component.find(CardInstrumentFieldset) - .prop('onUseNewInstrument')(); + component.find(CardInstrumentFieldset).prop('onUseNewInstrument')(); component.update(); - expect(paymentContext.hidePaymentSubmitButton) - .toHaveBeenCalledWith(defaultProps.method, true); + expect(paymentContext.hidePaymentSubmitButton).toHaveBeenCalledWith( + defaultProps.method, + true, + ); }); it('hides the payment submit button when a vaulted instrument is not selected', () => { - const component = mount(); + const component = mount(); - component.find(CardInstrumentFieldset) - .prop('onUseNewInstrument')(); + component.find(CardInstrumentFieldset).prop('onUseNewInstrument')(); component.update(); - expect(paymentContext.hidePaymentSubmitButton) - .toHaveBeenCalledWith(defaultProps.method, true); + expect(paymentContext.hidePaymentSubmitButton).toHaveBeenCalledWith( + defaultProps.method, + true, + ); }); it('shows fields on the Widget when you click Use another payment form on the vaulted instruments dropdown', () => { - const component = mount(); - component.find(CardInstrumentFieldset) - .prop('onSelectInstrument')(getInstruments()[0].bigpayToken); + const component = mount(); + + component.find(CardInstrumentFieldset).prop('onSelectInstrument')( + getInstruments()[0].bigpayToken, + ); component.update(); - component.find(CardInstrumentFieldset) - .prop('onUseNewInstrument')(); + component.find(CardInstrumentFieldset).prop('onUseNewInstrument')(); component.update(); - expect(component.find(CardInstrumentFieldset).prop('selectedInstrumentId')) - .toEqual(undefined); + expect(component.find(CardInstrumentFieldset).prop('selectedInstrumentId')).toBeUndefined(); }); it('switches to "use new card" view if all instruments are deleted', () => { - const component = mount(); + const component = mount(); - expect(component.find(CardInstrumentFieldset)) - .toHaveLength(1); + expect(component.find(CardInstrumentFieldset)).toHaveLength(1); // Update state checkoutState = merge({}, checkoutState, { @@ -275,13 +254,13 @@ describe('HostedDropInPaymentMethod', () => { subscribeEventEmitter.emit('change'); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - component.find(CardInstrumentFieldset) - .prop('onDeleteInstrument')!(getInstruments()[0].bigpayToken); + component.find(CardInstrumentFieldset).prop('onDeleteInstrument')!( + getInstruments()[0].bigpayToken, + ); component.update(); - expect(component.find('.paymentMethod--hosted')) - .toHaveLength(1); + expect(component.find('.paymentMethod--hosted')).toHaveLength(1); }); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/HostedDropInPaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/HostedDropInPaymentMethod.tsx index 72b8e004eb..ac4654710d 100644 --- a/packages/core/src/app/payment/paymentMethod/HostedDropInPaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/HostedDropInPaymentMethod.tsx @@ -1,23 +1,34 @@ -import { PaymentFormValues } from '@bigcommerce/checkout/payment-integration-api'; -import { CardInstrument, CheckoutSelectors, CustomerInitializeOptions, +import { + CardInstrument, + CheckoutSelectors, + CustomerInitializeOptions, CustomerRequestOptions, - Instrument, PaymentInitializeOptions, + Instrument, + PaymentInitializeOptions, PaymentInstrument, - PaymentMethod, PaymentRequestOptions } from '@bigcommerce/checkout-sdk'; + PaymentMethod, + PaymentRequestOptions, +} from '@bigcommerce/checkout-sdk'; import { memoizeOne } from '@bigcommerce/memoize'; import classNames from 'classnames'; import { find, noop, some } from 'lodash'; import React, { Component, ReactNode } from 'react'; -import { withCheckout, CheckoutContextProps } from '../../checkout'; +import { PaymentFormValues } from '@bigcommerce/checkout/payment-integration-api'; + +import { CheckoutContextProps, withCheckout } from '../../checkout'; import { connectFormik, ConnectFormikProps } from '../../common/form'; import { MapToPropsFactory } from '../../common/hoc'; import { LoadingOverlay } from '../../ui/loading'; -import { isBankAccountInstrument, isCardInstrument, isInstrumentCardCodeRequiredSelector, +import { + CardInstrumentFieldset, + CreditCardValidation, + isBankAccountInstrument, + isCardInstrument, + isInstrumentCardCodeRequiredSelector, isInstrumentCardNumberRequiredSelector, isInstrumentFeatureAvailable, - CardInstrumentFieldset, - CreditCardValidation } from '../storedInstrument'; +} from '../storedInstrument'; import withPayment, { WithPaymentProps } from '../withPayment'; export interface HostedDropInPaymentMethodProps { @@ -34,7 +45,10 @@ export interface HostedDropInPaymentMethodProps { deinitializePayment(options: PaymentRequestOptions): Promise; onUnhandledError?(error: Error): void; initializeCustomer?(options: CustomerInitializeOptions): Promise; - initializePayment(options: PaymentInitializeOptions, selectedInstrumentId?: string): Promise; + initializePayment( + options: PaymentInitializeOptions, + selectedInstrumentId?: string, + ): Promise; signInCustomer?(): void; onSignOut?(): void; onSignOutError?(error: Error): void; @@ -59,9 +73,9 @@ interface WithCheckoutHostedDropInPaymentMethodProps { class HostedDropInPaymentMethod extends Component< HostedDropInPaymentMethodProps & - WithCheckoutHostedDropInPaymentMethodProps & - ConnectFormikProps & - WithPaymentProps + WithCheckoutHostedDropInPaymentMethodProps & + ConnectFormikProps & + WithPaymentProps > { state: HostedDropInPaymentMethodState = { isAddingNewCard: false, @@ -77,14 +91,19 @@ class HostedDropInPaymentMethod extends Component< if (isInstrumentFeatureAvailableProp) { await loadInstruments(); } + await this.initializeMethod(); } catch (error) { onUnhandledError(error); } } - async componentDidUpdate(prevProps: Readonly, - prevState: Readonly): Promise { + async componentDidUpdate( + prevProps: Readonly< + HostedDropInPaymentMethodProps & WithCheckoutHostedDropInPaymentMethodProps + >, + prevState: Readonly, + ): Promise { const { deinitializePayment = noop, instruments, @@ -94,17 +113,16 @@ class HostedDropInPaymentMethod extends Component< isPaymentDataRequired, } = this.props; - const { - selectedInstrumentId, - isAddingNewCard, - } = this.state; + const { selectedInstrumentId, isAddingNewCard } = this.state; const selectedInstrument = this.getDefaultInstrumentId(); - hidePaymentSubmitButton(method, (!selectedInstrument && isPaymentDataRequired)); + hidePaymentSubmitButton(method, !selectedInstrument && isPaymentDataRequired); - if (selectedInstrumentId !== prevState.selectedInstrumentId || + if ( + selectedInstrumentId !== prevState.selectedInstrumentId || (prevProps.instruments.length > 0 && instruments.length === 0) || - isAddingNewCard !== prevState.isAddingNewCard) { + isAddingNewCard !== prevState.isAddingNewCard + ) { try { await deinitializePayment({ gatewayId: method.gateway, @@ -156,44 +174,44 @@ class HostedDropInPaymentMethod extends Component< shouldHideInstrumentExpiryDate = false, } = this.props; - const { - isAddingNewCard, - selectedInstrumentId = this.getDefaultInstrumentId(), - } = this.state; + const { isAddingNewCard, selectedInstrumentId = this.getDefaultInstrumentId() } = + this.state; - const shouldShowInstrumentFieldset = isInstrumentFeatureAvailableProp && instruments.length > 0; + const shouldShowInstrumentFieldset = + isInstrumentFeatureAvailableProp && instruments.length > 0; const shouldShowCreditCardFieldset = !shouldShowInstrumentFieldset || isAddingNewCard; const isLoading = (isInitializing || isLoadingInstruments) && !hideWidget; return ( - - { shouldShowInstrumentFieldset && } - - { shouldShowCreditCardFieldset &&
    -
    + {shouldShowInstrumentFieldset && ( + -
    } + )} + + {shouldShowCreditCardFieldset && ( +
    +
    +
    + )} ); } @@ -206,10 +224,8 @@ class HostedDropInPaymentMethod extends Component< } const { instruments } = this.props; - const defaultInstrument = ( - instruments.find(instrument => instrument.defaultInstrument) || - instruments[0] - ); + const defaultInstrument = + instruments.find((instrument) => instrument.defaultInstrument) || instruments[0]; return defaultInstrument && defaultInstrument.bigpayToken; } @@ -226,8 +242,12 @@ class HostedDropInPaymentMethod extends Component< const { selectedInstrumentId = this.getDefaultInstrumentId() } = this.state; const selectedInstrument = find(instruments, { bigpayToken: selectedInstrumentId }); - const shouldShowNumberField = selectedInstrument ? isInstrumentCardNumberRequiredProp(selectedInstrument as CardInstrument) : false; - const shouldShowCardCodeField = selectedInstrument ? isInstrumentCardCodeRequiredProp(selectedInstrument as CardInstrument, method) : false; + const shouldShowNumberField = selectedInstrument + ? isInstrumentCardNumberRequiredProp(selectedInstrument as CardInstrument) + : false; + const shouldShowCardCodeField = selectedInstrument + ? isInstrumentCardCodeRequiredProp(selectedInstrument as CardInstrument, method) + : false; if (hideVerificationFields) { return; @@ -239,8 +259,8 @@ class HostedDropInPaymentMethod extends Component< return ( ); } @@ -277,14 +297,20 @@ class HostedDropInPaymentMethod extends Component< setSubmit(method, null); - return initializePayment({ - gatewayId: method.gateway, - methodId: method.id, - }, selectedInstrumentId); + return initializePayment( + { + gatewayId: method.gateway, + methodId: method.id, + }, + selectedInstrumentId, + ); } - private handleDeleteInstrument: (id: string) => void = id => { - const { instruments, formik: { setFieldValue } } = this.props; + private handleDeleteInstrument: (id: string) => void = (id) => { + const { + instruments, + formik: { setFieldValue }, + } = this.props; const { selectedInstrumentId } = this.state; if (instruments.length === 0) { @@ -303,7 +329,7 @@ class HostedDropInPaymentMethod extends Component< } }; - private handleSelectInstrument: (id: string) => void = id => { + private handleSelectInstrument: (id: string) => void = (id) => { this.setState({ isAddingNewCard: false, selectedInstrumentId: id, @@ -311,11 +337,7 @@ class HostedDropInPaymentMethod extends Component< }; private handleUseNewCard: () => void = async () => { - const { - deinitializePayment = noop, - initializePayment = noop, - method, - } = this.props; + const { deinitializePayment = noop, initializePayment = noop, method } = this.props; this.setState({ isAddingNewCard: true, @@ -334,31 +356,25 @@ class HostedDropInPaymentMethod extends Component< }; } -const mapFromCheckoutProps: MapToPropsFactory> = () => { - const filterInstruments = memoizeOne((instruments: PaymentInstrument[] = []) => instruments.filter(instrument => isCardInstrument(instrument) || isBankAccountInstrument(instrument))); + HostedDropInPaymentMethodProps & ConnectFormikProps +> = () => { + const filterInstruments = memoizeOne((instruments: PaymentInstrument[] = []) => + instruments.filter( + (instrument) => isCardInstrument(instrument) || isBankAccountInstrument(instrument), + ), + ); return (context, props) => { + const { isUsingMultiShipping = false, method } = props; - const { - isUsingMultiShipping = false, - method, - } = props; - - const {checkoutService, checkoutState} = context; + const { checkoutService, checkoutState } = context; const { - data: { - getCheckout, - getConfig, - getCustomer, - getInstruments, - isPaymentDataRequired, - }, - statuses: { - isLoadingInstruments, - }, + data: { getCheckout, getConfig, getCustomer, getInstruments, isPaymentDataRequired }, + statuses: { isLoadingInstruments }, } = checkoutState; const checkout = getCheckout(); @@ -373,7 +389,7 @@ const mapFromCheckoutProps: MapToPropsFactory { let HostedFieldPaymentMethodTest: FunctionComponent; @@ -26,95 +28,79 @@ describe('HostedFieldPaymentMethod', () => { localeContext = createLocaleContext(getStoreConfig()); - HostedFieldPaymentMethodTest = props => ( - - - + HostedFieldPaymentMethodTest = (props) => ( + + + ); }); it('initializes payment method when component mounts', () => { - mount(); + mount(); - expect(defaultProps.initializePayment) - .toHaveBeenCalled(); + expect(defaultProps.initializePayment).toHaveBeenCalled(); }); it('deinitializes payment method when component unmounts', () => { - const component = mount(); + const component = mount(); - expect(defaultProps.deinitializePayment) - .not.toHaveBeenCalled(); + expect(defaultProps.deinitializePayment).not.toHaveBeenCalled(); component.unmount(); - expect(defaultProps.deinitializePayment) - .toHaveBeenCalled(); + expect(defaultProps.deinitializePayment).toHaveBeenCalled(); }); it('renders loading overlay while waiting for method to initialize', () => { let component: ReactWrapper; - component = mount(); + component = mount(); - expect(component.find(LoadingOverlay).prop('isLoading')) - .toEqual(true); + expect(component.find(LoadingOverlay).prop('isLoading')).toBe(true); - component = mount(); + component = mount(); - expect(component.find(LoadingOverlay).prop('isLoading')) - .toEqual(false); + expect(component.find(LoadingOverlay).prop('isLoading')).toBe(false); }); it('renders card number placeholder', () => { - const component = mount(); + const component = mount(); - expect(component.exists(`#${defaultProps.cardNumberId}`)) - .toEqual(true); + expect(component.exists(`#${defaultProps.cardNumberId}`)).toBe(true); }); it('renders card expiry placeholder', () => { - const component = mount(); + const component = mount(); - expect(component.exists(`#${defaultProps.cardExpiryId}`)) - .toEqual(true); + expect(component.exists(`#${defaultProps.cardExpiryId}`)).toBe(true); }); it('renders card cvv placeholder if configured', () => { - const component = mount(); + const component = mount( + , + ); - expect(component.exists('#card-code')) - .toEqual(true); + expect(component.exists('#card-code')).toBe(true); }); it('renders postal code placeholder if configured', () => { - const component = mount(); + const component = mount( + , + ); - expect(component.exists('#postal-code')) - .toEqual(true); + expect(component.exists('#postal-code')).toBe(true); }); it('renders wallet button placeholder if required', () => { - const component = mount( } - />); + const component = mount( + } + />, + ); - expect(component.exists('#wallet-button')) - .toEqual(true); + expect(component.exists('#wallet-button')).toBe(true); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/HostedFieldPaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/HostedFieldPaymentMethod.tsx index 4f86ba772b..38dc2604f2 100644 --- a/packages/core/src/app/payment/paymentMethod/HostedFieldPaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/HostedFieldPaymentMethod.tsx @@ -1,4 +1,9 @@ -import { CheckoutSelectors, PaymentInitializeOptions, PaymentMethod, PaymentRequestOptions } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + PaymentInitializeOptions, + PaymentMethod, + PaymentRequestOptions, +} from '@bigcommerce/checkout-sdk'; import { noop } from 'lodash'; import React, { Component, ReactNode } from 'react'; @@ -22,11 +27,7 @@ export interface HostedFieldPaymentMethodProps { // TODO: Use HostedCreditCardFieldset export default class HostedFieldPaymentMethod extends Component { async componentDidMount(): Promise { - const { - initializePayment, - method, - onUnhandledError = noop, - } = this.props; + const { initializePayment, method, onUnhandledError = noop } = this.props; try { await initializePayment({ @@ -39,11 +40,7 @@ export default class HostedFieldPaymentMethod extends Component { - const { - deinitializePayment, - method, - onUnhandledError = noop, - } = this.props; + const { deinitializePayment, method, onUnhandledError = noop } = this.props; try { await deinitializePayment({ @@ -66,21 +63,16 @@ export default class HostedFieldPaymentMethod extends Component +
    - { walletButtons && - { walletButtons } - } + {walletButtons && {walletButtons}} -
    +
    @@ -88,24 +80,28 @@ export default class HostedFieldPaymentMethod extends Component -
    +
    - { cardCodeId && - - -
    - } - - { postalCodeId && - - -
    - } + {cardCodeId && ( + + + +
    + + )} + + {postalCodeId && ( + + + +
    + + )}
    ); diff --git a/packages/core/src/app/payment/paymentMethod/HostedPaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/HostedPaymentMethod.spec.tsx index f86dd7dc45..19904aadcf 100644 --- a/packages/core/src/app/payment/paymentMethod/HostedPaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/HostedPaymentMethod.spec.tsx @@ -1,4 +1,9 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService, StoreConfig } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, + StoreConfig, +} from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import { Formik } from 'formik'; import { noop } from 'lodash'; @@ -11,9 +16,9 @@ import { getCustomer } from '../../customer/customers.mock'; import { createLocaleContext, LocaleContext, LocaleContextType } from '../../locale'; import { LoadingOverlay } from '../../ui/loading'; import { getPaymentMethod } from '../payment-methods.mock'; +import PaymentContext, { PaymentContextProps } from '../PaymentContext'; import { AccountInstrumentFieldset } from '../storedInstrument'; import { getInstruments } from '../storedInstrument/instruments.mock'; -import PaymentContext, { PaymentContextProps } from '../PaymentContext'; import HostedPaymentMethod, { HostedPaymentMethodProps } from './HostedPaymentMethod'; @@ -46,27 +51,20 @@ describe('HostedPaymentMethod', () => { hidePaymentSubmitButton: jest.fn(), }; - jest.spyOn(checkoutState.data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(checkoutState.data, 'getCart').mockReturnValue(getCart()); - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(storeConfig); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(storeConfig); - jest.spyOn(checkoutState.data, 'getCustomer') - .mockReturnValue(getCustomer()); + jest.spyOn(checkoutState.data, 'getCustomer').mockReturnValue(getCustomer()); - jest.spyOn(checkoutState.data, 'isPaymentDataRequired') - .mockReturnValue(true); + jest.spyOn(checkoutState.data, 'isPaymentDataRequired').mockReturnValue(true); - HostedPaymentMethodTest = props => ( - - - - - + HostedPaymentMethodTest = (props) => ( + + + + + @@ -75,131 +73,107 @@ describe('HostedPaymentMethod', () => { }); it('initializes payment method when component mounts', () => { - mount(); + mount(); - expect(defaultProps.initializePayment) - .toHaveBeenCalled(); + expect(defaultProps.initializePayment).toHaveBeenCalled(); }); it('deinitializes payment method when component unmounts', () => { - const component = mount(); + const component = mount(); - expect(defaultProps.deinitializePayment) - .not.toHaveBeenCalled(); + expect(defaultProps.deinitializePayment).not.toHaveBeenCalled(); component.unmount(); - expect(defaultProps.deinitializePayment) - .toHaveBeenCalled(); + expect(defaultProps.deinitializePayment).toHaveBeenCalled(); }); it('renders loading overlay while waiting for method to initialize if description is provided', () => { let component: ReactWrapper; - component = mount(); + component = mount( + , + ); - expect(component.find(LoadingOverlay).prop('isLoading')) - .toEqual(true); + expect(component.find(LoadingOverlay).prop('isLoading')).toBe(true); - component = mount(); + component = mount(); - expect(component.find(LoadingOverlay).prop('isLoading')) - .toEqual(false); + expect(component.find(LoadingOverlay).prop('isLoading')).toBe(false); }); it('does not render loading overlay if there is no description', () => { - const component = mount(); + const component = mount(); - expect(component.find(LoadingOverlay)) - .toHaveLength(0); + expect(component.find(LoadingOverlay)).toHaveLength(0); }); describe('if stored instrument feature is available', () => { beforeEach(() => { defaultProps.method.config.isVaultingEnabled = true; - jest.spyOn(checkoutState.data, 'getInstruments') - .mockReturnValue(getInstruments()); + jest.spyOn(checkoutState.data, 'getInstruments').mockReturnValue(getInstruments()); - jest.spyOn(checkoutService, 'initializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'initializePayment').mockResolvedValue(checkoutState); - jest.spyOn(checkoutService, 'loadInstruments') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'loadInstruments').mockResolvedValue(checkoutState); }); it('loads stored instruments when component mounts', async () => { - mount(); + mount(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(checkoutService.loadInstruments) - .toHaveBeenCalled(); + expect(checkoutService.loadInstruments).toHaveBeenCalled(); }); it('shows instruments fieldset when there is at least one stored instrument', () => { - const component = mount(); + const component = mount(); - expect(component.find(AccountInstrumentFieldset)) - .toHaveLength(1); + expect(component.find(AccountInstrumentFieldset)).toHaveLength(1); }); it('shows the instrument fieldset when there are instruments, but no trusted stored instruments', () => { jest.spyOn(checkoutState.data, 'getInstruments').mockReturnValue( - getInstruments().map(instrument => ({ ...instrument, trustedShippingAddress: false })) + getInstruments().map((instrument) => ({ + ...instrument, + trustedShippingAddress: false, + })), ); - const component = mount(); + const component = mount(); - expect(component.find(AccountInstrumentFieldset)) - .toHaveLength(1); + expect(component.find(AccountInstrumentFieldset)).toHaveLength(1); }); it('does not show instruments fieldset when there are no stored instruments', () => { - jest.spyOn(checkoutState.data, 'getInstruments') - .mockReturnValue([]); + jest.spyOn(checkoutState.data, 'getInstruments').mockReturnValue([]); - const component = mount(); + const component = mount(); - expect(component.find(AccountInstrumentFieldset)) - .toHaveLength(0); + expect(component.find(AccountInstrumentFieldset)).toHaveLength(0); }); it('does not show instruments fieldset when starting from the cart', () => { - jest.spyOn(checkoutState.data, 'isPaymentDataSubmitted') - .mockReturnValue(true); + jest.spyOn(checkoutState.data, 'isPaymentDataSubmitted').mockReturnValue(true); - const component = mount(); + const component = mount(); - expect(component.find(AccountInstrumentFieldset)) - .toHaveLength(0); + expect(component.find(AccountInstrumentFieldset)).toHaveLength(0); }); it('shows save account checkbox when there are no stored instruments', () => { - jest.spyOn(checkoutState.data, 'getInstruments') - .mockReturnValue([]); + jest.spyOn(checkoutState.data, 'getInstruments').mockReturnValue([]); - const container = mount(); + const container = mount(); - expect(container.find('input[name="shouldSaveInstrument"]').exists()) - .toBe(true); + expect(container.find('input[name="shouldSaveInstrument"]').exists()).toBe(true); }); it('uses PaymentMethod to retrieve instruments', () => { - mount(); + mount(); - expect(checkoutState.data.getInstruments) - .toHaveBeenCalledWith(defaultProps.method); + expect(checkoutState.data.getInstruments).toHaveBeenCalledWith(defaultProps.method); }); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/HostedPaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/HostedPaymentMethod.tsx index 333d5ac85b..df30ee4b8c 100644 --- a/packages/core/src/app/payment/paymentMethod/HostedPaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/HostedPaymentMethod.tsx @@ -1,17 +1,29 @@ -import { AccountInstrument, CheckoutSelectors, PaymentInitializeOptions, PaymentInstrument, PaymentMethod, PaymentRequestOptions } from '@bigcommerce/checkout-sdk'; -import { PaymentFormValues } from '@bigcommerce/checkout/payment-integration-api'; +import { + AccountInstrument, + CheckoutSelectors, + PaymentInitializeOptions, + PaymentInstrument, + PaymentMethod, + PaymentRequestOptions, +} from '@bigcommerce/checkout-sdk'; import { memoizeOne } from '@bigcommerce/memoize'; import { find, noop } from 'lodash'; import React, { Component, ReactNode } from 'react'; -import { withCheckout, CheckoutContextProps } from '../../checkout'; +import { PaymentFormValues } from '@bigcommerce/checkout/payment-integration-api'; + +import { CheckoutContextProps, withCheckout } from '../../checkout'; import { connectFormik, ConnectFormikProps } from '../../common/form'; import { MapToPropsFactory } from '../../common/hoc'; import { withLanguage, WithLanguageProps } from '../../locale'; import { LoadingOverlay } from '../../ui/loading'; -import { isAccountInstrument, isInstrumentFeatureAvailable, AccountInstrumentFieldset } from '../storedInstrument'; -import withPayment, { WithPaymentProps } from '../withPayment'; +import { + AccountInstrumentFieldset, + isAccountInstrument, + isInstrumentFeatureAvailable, +} from '../storedInstrument'; import StoreInstrumentFieldset from '../StoreInstrumentFieldset'; +import withPayment, { WithPaymentProps } from '../withPayment'; export interface HostedPaymentMethodProps { description?: ReactNode; @@ -73,11 +85,7 @@ class HostedPaymentMethod extends Component< } async componentWillUnmount(): Promise { - const { - deinitializePayment, - method, - onUnhandledError = noop, - } = this.props; + const { deinitializePayment, method, onUnhandledError = noop } = this.props; try { await deinitializePayment({ @@ -99,36 +107,36 @@ class HostedPaymentMethod extends Component< isInstrumentFeatureAvailable: isInstrumentFeatureAvailableProp, } = this.props; - const { - selectedInstrument = this.getDefaultInstrument(), - } = this.state; + const { selectedInstrument = this.getDefaultInstrument() } = this.state; const isLoading = isInitializing || isLoadingInstruments; - const shouldShowInstrumentFieldset = isInstrumentFeatureAvailableProp && (instruments.length > 0 || isNewAddress); + const shouldShowInstrumentFieldset = + isInstrumentFeatureAvailableProp && (instruments.length > 0 || isNewAddress); if (!description && !isInstrumentFeatureAvailableProp) { return null; } return ( - +
    - { description } - - { shouldShowInstrumentFieldset && } - - { isInstrumentFeatureAvailableProp && } + {description} + + {shouldShowInstrumentFieldset && ( + + )} + + {isInstrumentFeatureAvailableProp && ( + + )}
    ); @@ -152,10 +160,8 @@ class HostedPaymentMethod extends Component< }); }; - private handleSelectInstrument: (id: string) => void = id => { - const { - instruments, - } = this.props; + private handleSelectInstrument: (id: string) => void = (id) => { + const { instruments } = this.props; this.setState({ isAddingNewInstrument: false, @@ -169,14 +175,15 @@ const mapFromCheckoutProps: MapToPropsFactory< WithCheckoutHostedPaymentMethodProps, HostedPaymentMethodProps & ConnectFormikProps > = () => { - const filterAccountInstruments = memoizeOne((instruments: PaymentInstrument[] = []) => instruments.filter(isAccountInstrument)); - const filterTrustedInstruments = memoizeOne((instruments: AccountInstrument[] = []) => instruments.filter(({ trustedShippingAddress }) => trustedShippingAddress)); + const filterAccountInstruments = memoizeOne((instruments: PaymentInstrument[] = []) => + instruments.filter(isAccountInstrument), + ); + const filterTrustedInstruments = memoizeOne((instruments: AccountInstrument[] = []) => + instruments.filter(({ trustedShippingAddress }) => trustedShippingAddress), + ); return (context, props) => { - const { - isUsingMultiShipping = false, - method, - } = props; + const { isUsingMultiShipping = false, method } = props; const { checkoutService, checkoutState } = context; @@ -189,9 +196,7 @@ const mapFromCheckoutProps: MapToPropsFactory< isPaymentDataRequired, isPaymentDataSubmitted, }, - statuses: { - isLoadingInstruments, - }, + statuses: { isLoadingInstruments }, } = checkoutState; const cart = getCart(); @@ -208,8 +213,9 @@ const mapFromCheckoutProps: MapToPropsFactory< return { instruments: trustedInstruments, isNewAddress: trustedInstruments.length === 0 && currentMethodInstruments.length > 0, - isInstrumentFeatureAvailable: !isPaymentDataSubmitted(method.id, method.gateway) - && isInstrumentFeatureAvailable({ + isInstrumentFeatureAvailable: + !isPaymentDataSubmitted(method.id, method.gateway) && + isInstrumentFeatureAvailable({ config, customer, isUsingMultiShipping, @@ -222,4 +228,6 @@ const mapFromCheckoutProps: MapToPropsFactory< }; }; -export default connectFormik(withLanguage(withPayment(withCheckout(mapFromCheckoutProps)(HostedPaymentMethod)))); +export default connectFormik( + withLanguage(withPayment(withCheckout(mapFromCheckoutProps)(HostedPaymentMethod))), +); diff --git a/packages/core/src/app/payment/paymentMethod/HostedWidgetPaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/HostedWidgetPaymentMethod.spec.tsx index 2043e1125b..32d1dd54db 100644 --- a/packages/core/src/app/payment/paymentMethod/HostedWidgetPaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/HostedWidgetPaymentMethod.spec.tsx @@ -1,4 +1,9 @@ -import { createCheckoutService, BankInstrument, CheckoutSelectors, CheckoutService } from '@bigcommerce/checkout-sdk'; +import { + BankInstrument, + CheckoutSelectors, + CheckoutService, + createCheckoutService, +} from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import { Formik } from 'formik'; import { noop } from 'lodash'; @@ -10,16 +15,23 @@ import { CheckoutProvider } from '../../checkout'; import { getCheckout, getCheckoutPayment } from '../../checkout/checkouts.mock'; import { getStoreConfig } from '../../config/config.mock'; import { getCustomer } from '../../customer/customers.mock'; -import { createLocaleContext, LocaleContext, LocaleContextType, TranslatedString } from '../../locale'; +import { + createLocaleContext, + LocaleContext, + LocaleContextType, + TranslatedString, +} from '../../locale'; import { getConsignment } from '../../shipping/consignment.mock'; import { LoadingOverlay } from '../../ui/loading'; import { getCreditCardValidationSchema } from '../creditCard'; import { getPaymentMethod } from '../payment-methods.mock'; +import PaymentContext, { PaymentContextProps } from '../PaymentContext'; import { AccountInstrumentFieldset, CardInstrumentFieldset } from '../storedInstrument'; import { getInstruments } from '../storedInstrument/instruments.mock'; -import PaymentContext, { PaymentContextProps } from '../PaymentContext'; -import HostedWidgetPaymentMethod, { HostedWidgetPaymentMethodProps } from './HostedWidgetPaymentMethod'; +import HostedWidgetPaymentMethod, { + HostedWidgetPaymentMethodProps, +} from './HostedWidgetPaymentMethod'; import SignOutLink, { SignOutLinkProps } from './SignOutLink'; describe('HostedWidgetPaymentMethod', () => { @@ -48,109 +60,86 @@ describe('HostedWidgetPaymentMethod', () => { }; localeContext = createLocaleContext(getStoreConfig()); - jest.spyOn(checkoutState.data, 'getCheckout') - .mockReturnValue(getCheckout()); + jest.spyOn(checkoutState.data, 'getCheckout').mockReturnValue(getCheckout()); - jest.spyOn(checkoutState.data, 'isPaymentDataRequired') - .mockReturnValue(true); + jest.spyOn(checkoutState.data, 'isPaymentDataRequired').mockReturnValue(true); - jest.spyOn(checkoutState.data, 'getCart') - .mockReturnValue(getCart()); + jest.spyOn(checkoutState.data, 'getCart').mockReturnValue(getCart()); - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - jest.spyOn(checkoutState.data, 'getConsignments') - .mockReturnValue([getConsignment()]); + jest.spyOn(checkoutState.data, 'getConsignments').mockReturnValue([getConsignment()]); - jest.spyOn(checkoutState.data, 'getCustomer') - .mockReturnValue(getCustomer()); + jest.spyOn(checkoutState.data, 'getCustomer').mockReturnValue(getCustomer()); - jest.spyOn(checkoutState.data, 'isPaymentDataRequired') - .mockReturnValue(true); + jest.spyOn(checkoutState.data, 'isPaymentDataRequired').mockReturnValue(true); - HostedWidgetPaymentMethodTest = props => ( - - - - - + HostedWidgetPaymentMethodTest = (props) => ( + + + + + - + ); }); it('initializes payment method when component mounts', () => { - mount(); + mount(); - expect(defaultProps.initializePayment) - .toHaveBeenCalled(); + expect(defaultProps.initializePayment).toHaveBeenCalled(); expect(paymentContext.setSubmit).toHaveBeenCalled(); }); it('deinitializes payment method when component unmounts', () => { - const component = mount(); + const component = mount(); - expect(defaultProps.deinitializePayment) - .not.toHaveBeenCalled(); + expect(defaultProps.deinitializePayment).not.toHaveBeenCalled(); component.unmount(); - expect(defaultProps.deinitializePayment) - .toHaveBeenCalled(); + expect(defaultProps.deinitializePayment).toHaveBeenCalled(); }); it('does not initialize payment method if payment data is not required', () => { - jest.spyOn(checkoutState.data, 'isPaymentDataRequired') - .mockReturnValue(false); + jest.spyOn(checkoutState.data, 'isPaymentDataRequired').mockReturnValue(false); - mount(); + mount(); - expect(defaultProps.initializePayment) - .not.toHaveBeenCalled(); + expect(defaultProps.initializePayment).not.toHaveBeenCalled(); }); it('renders loading overlay while waiting for method to initialize', () => { let component: ReactWrapper; - component = mount(); + component = mount(); - expect(component.find(LoadingOverlay).prop('isLoading')) - .toEqual(true); + expect(component.find(LoadingOverlay).prop('isLoading')).toBe(true); - component = mount(); + component = mount(); - expect(component.find(LoadingOverlay).prop('isLoading')) - .toEqual(false); + expect(component.find(LoadingOverlay).prop('isLoading')).toBe(false); }); it('hides content while loading', () => { - const component = mount(); + const component = mount(); - expect(component.find(LoadingOverlay).prop('hideContentWhenLoading')) - .toEqual(true); + expect(component.find(LoadingOverlay).prop('hideContentWhenLoading')).toBe(true); }); it('renders placeholder container with provided ID', () => { - const component = mount(); + const component = mount(); - expect(component.exists(`#${defaultProps.containerId}`)) - .toEqual(true); + expect(component.exists(`#${defaultProps.containerId}`)).toBe(true); }); it('does not render sign out link', () => { - const component = mount(); + const component = mount(); - expect(component.find(SignOutLink)) - .toHaveLength(0); + expect(component.find(SignOutLink)).toHaveLength(0); }); it('shows the payment descriptor', () => { @@ -160,7 +149,7 @@ describe('HostedWidgetPaymentMethod', () => { paymentDescriptor: 'meow', }; - const component = mount(); + const component = mount(); expect(component.find('.payment-descriptor')).toHaveLength(1); }); @@ -172,13 +161,13 @@ describe('HostedWidgetPaymentMethod', () => { paymentDescriptor: 'meow', }; - const component = mount(); + const component = mount(); expect(component.find('.payment-descriptor')).toHaveLength(0); }); it('does not render the component', () => { - const component = mount(); + const component = mount(); expect(component.isEmptyRender()).toBe(false); @@ -194,7 +183,7 @@ describe('HostedWidgetPaymentMethod', () => { defaultProps = { ...defaultProps, - method : { + method: { ...getPaymentMethod(), id: 'card', }, @@ -202,7 +191,7 @@ describe('HostedWidgetPaymentMethod', () => { shouldRenderCustomInstrument: true, }; - const component = mount(); + const component = mount(); expect(component.find('[id="custom-form-id"]')).toHaveLength(1); }); @@ -210,15 +199,15 @@ describe('HostedWidgetPaymentMethod', () => { it('does execute validateCustomRender', () => { defaultProps = { ...defaultProps, - method : { + method: { ...getPaymentMethod(), id: 'card', }, renderCustomPaymentForm: jest.fn(), - shouldRenderCustomInstrument : true, + shouldRenderCustomInstrument: true, }; - mount(); + mount(); expect(defaultProps.renderCustomPaymentForm).toHaveBeenCalled(); }); @@ -230,90 +219,91 @@ describe('HostedWidgetPaymentMethod', () => { defaultProps = { ...defaultProps, - method : { + method: { ...getPaymentMethod(), id: 'card', }, renderCustomPaymentForm: () => , - shouldRenderCustomInstrument : false, + shouldRenderCustomInstrument: false, }; - const component = mount(); + const component = mount(); expect(component.find('[id="custom-form-id"]')).toHaveLength(0); - }); describe('when user is signed into their payment method account', () => { beforeEach(() => { - jest.spyOn(checkoutState.data, 'getCheckout') - .mockReturnValue({ - ...getCheckout(), - payments: [ - { ...getCheckoutPayment(), providerId: defaultProps.method.id }, - ], - }); + jest.spyOn(checkoutState.data, 'getCheckout').mockReturnValue({ + ...getCheckout(), + payments: [{ ...getCheckoutPayment(), providerId: defaultProps.method.id }], + }); }); it('renders sign out link if user is signed into their payment method account', () => { - const component = mount(); + const component = mount(); - expect(component.find(SignOutLink)) - .toHaveLength(1); + expect(component.find(SignOutLink)).toHaveLength(1); }); it('signs out from payment method account of user when clicking on sign out link', async () => { const handleSignOutError = jest.fn(); - jest.spyOn(checkoutService, 'signOutCustomer') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'signOutCustomer').mockResolvedValue(checkoutState); - const component = mount(); + const component = mount( + , + ); (component.find(SignOutLink) as ReactWrapper).prop('onSignOut')(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(checkoutService.signOutCustomer) - .toHaveBeenCalledWith({ methodId: defaultProps.method.id }); + expect(checkoutService.signOutCustomer).toHaveBeenCalledWith({ + methodId: defaultProps.method.id, + }); - expect(handleSignOutError) - .not.toHaveBeenCalled(); + expect(handleSignOutError).not.toHaveBeenCalled(); }); it('notifies parent component if unable to sign out', async () => { const handleSignOutError = jest.fn(); - jest.spyOn(checkoutService, 'signOutCustomer') - .mockRejectedValue(new Error('Unknown error')); + jest.spyOn(checkoutService, 'signOutCustomer').mockRejectedValue( + new Error('Unknown error'), + ); - const component = mount(); + const component = mount( + , + ); (component.find(SignOutLink) as ReactWrapper).prop('onSignOut')(); - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); - expect(handleSignOutError) - .toHaveBeenCalledWith(expect.any(Error)); + expect(handleSignOutError).toHaveBeenCalledWith(expect.any(Error)); }); it('renders link for user to edit their selected credit card', () => { const payMethodId = 'walletButton'; - const component = mount(); - - expect(component.find(`#${payMethodId}`).find(TranslatedString).prop('id')) - .toEqual('remote.select_different_card_action'); + const component = mount( + , + ); + + expect(component.find(`#${payMethodId}`).find(TranslatedString).prop('id')).toBe( + 'remote.select_different_card_action', + ); }); }); @@ -329,26 +319,27 @@ describe('HostedWidgetPaymentMethod', () => { }); it('asks user to sign in when payment form is submitted', () => { - mount(); + mount(); - expect(defaultProps.initializeCustomer) - .toHaveBeenCalled(); + expect(defaultProps.initializeCustomer).toHaveBeenCalled(); - expect(paymentContext.setSubmit) - .toHaveBeenCalledWith(defaultProps.method, defaultProps.signInCustomer); + expect(paymentContext.setSubmit).toHaveBeenCalledWith( + defaultProps.method, + defaultProps.signInCustomer, + ); }); it('does not ask user to sign in if payment data is not required', () => { - jest.spyOn(checkoutState.data, 'isPaymentDataRequired') - .mockReturnValue(false); + jest.spyOn(checkoutState.data, 'isPaymentDataRequired').mockReturnValue(false); - mount(); + mount(); - expect(defaultProps.initializeCustomer) - .not.toHaveBeenCalled(); + expect(defaultProps.initializeCustomer).not.toHaveBeenCalled(); - expect(paymentContext.setSubmit) - .not.toHaveBeenCalledWith(defaultProps.method, defaultProps.signInCustomer); + expect(paymentContext.setSubmit).not.toHaveBeenCalledWith( + defaultProps.method, + defaultProps.signInCustomer, + ); }); }); @@ -356,69 +347,58 @@ describe('HostedWidgetPaymentMethod', () => { beforeEach(() => { defaultProps.method.config.isVaultingEnabled = true; - jest.spyOn(checkoutState.data, 'getInstruments') - .mockReturnValue(getInstruments()); - jest.spyOn(checkoutService, 'loadInstruments') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutState.data, 'getInstruments').mockReturnValue(getInstruments()); + jest.spyOn(checkoutService, 'loadInstruments').mockResolvedValue(checkoutState); }); it('loads stored instruments when component mounts', () => { - mount(); + mount(); - expect(checkoutService.loadInstruments) - .toHaveBeenCalled(); + expect(checkoutService.loadInstruments).toHaveBeenCalled(); }); it('only shows instruments fieldset when there is at least one stored instrument', () => { - const component = mount(); + const component = mount(); - expect(component.find(CardInstrumentFieldset)) - .toHaveLength(1); + expect(component.find(CardInstrumentFieldset)).toHaveLength(1); }); it('uses PaymentMethod to retrieve instruments', () => { - mount(); + mount(); - expect(checkoutState.data.getInstruments) - .toHaveBeenCalledWith(defaultProps.method); + expect(checkoutState.data.getInstruments).toHaveBeenCalledWith(defaultProps.method); }); it('shows hosted widget and save credit card form when there are no stored instruments', () => { - jest.spyOn(checkoutState.data, 'getInstruments') - .mockReturnValue([]); + jest.spyOn(checkoutState.data, 'getInstruments').mockReturnValue([]); - const container = mount(); + const container = mount(); const hostedWidgetComponent = container.find('#widget-container'); - expect(hostedWidgetComponent) - .toHaveLength(1); + expect(hostedWidgetComponent).toHaveLength(1); expect(container.text()).toMatch(/save/i); expect(container.text()).toMatch(/card/i); expect(container.text()).not.toMatch(/account/i); - expect(container.find('input[name="shouldSaveInstrument"]').exists()) - .toBe(true); + expect(container.find('input[name="shouldSaveInstrument"]').exists()).toBe(true); }); it('shows save account checkbox when has isAccountInstrument prop', () => { defaultProps.isAccountInstrument = true; - jest.spyOn(checkoutState.data, 'getInstruments') - .mockReturnValue([]); + jest.spyOn(checkoutState.data, 'getInstruments').mockReturnValue([]); - const container = mount(); + const container = mount(); const hostedWidgetComponent = container.find('#widget-container'); - expect(hostedWidgetComponent) - .toHaveLength(1); + expect(hostedWidgetComponent).toHaveLength(1); expect(container.text()).toMatch(/save/i); expect(container.text()).toMatch(/account/i); expect(container.text()).not.toMatch(/card/i); - expect(container.find('input[name="shouldSaveInstrument"]').exists()) - .toBe(true); + expect(container.find('input[name="shouldSaveInstrument"]').exists()).toBe(true); }); it('shows fields on the Widget when you click Use another payment form on the vaulted bank account instruments dropdown', () => { @@ -438,20 +418,17 @@ describe('HostedWidgetPaymentMethod', () => { iban: '12345', }, ]; - jest.spyOn(checkoutState.data, 'getInstruments') - .mockReturnValue(mockBankInstrument); - const container = mount(); + jest.spyOn(checkoutState.data, 'getInstruments').mockReturnValue(mockBankInstrument); + + const container = mount(); - container.find(AccountInstrumentFieldset) - .prop('onUseNewInstrument')(); + container.find(AccountInstrumentFieldset).prop('onUseNewInstrument')(); container.update(); - expect(container.find('.form-field--saveInstrument')) - .toHaveLength(1); + expect(container.find('.form-field--saveInstrument')).toHaveLength(1); - expect(container.find('.form-field--saveInstrument')) - .toHaveLength(1); + expect(container.find('.form-field--saveInstrument')).toHaveLength(1); }); it('sets validation schema when component mounts', () => { @@ -460,18 +437,21 @@ describe('HostedWidgetPaymentMethod', () => { language: localeContext.language, }); - mount(); + mount( + , + ); - expect(paymentContext.setValidationSchema) - .toHaveBeenCalled(); + expect(paymentContext.setValidationSchema).toHaveBeenCalled(); - const schema: Schema = (paymentContext.setValidationSchema as jest.Mock).mock.calls[0][1]; + const schema: Schema = (paymentContext.setValidationSchema as jest.Mock).mock + .calls[0][1]; - expect(Object.keys(schema.describe().fields)) - .toEqual(Object.keys(expectedSchema.describe().fields)); - }); + expect(Object.keys(schema.describe().fields)).toEqual( + Object.keys(expectedSchema.describe().fields), + ); + }); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/HostedWidgetPaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/HostedWidgetPaymentMethod.tsx index c2372fb15f..e94f80742c 100644 --- a/packages/core/src/app/payment/paymentMethod/HostedWidgetPaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/HostedWidgetPaymentMethod.tsx @@ -1,20 +1,40 @@ -import { PaymentFormValues } from '@bigcommerce/checkout/payment-integration-api'; -import { AccountInstrument, CardInstrument, CheckoutSelectors, CustomerInitializeOptions, CustomerRequestOptions, Instrument, PaymentInitializeOptions, PaymentInstrument, PaymentMethod, PaymentRequestOptions } from '@bigcommerce/checkout-sdk'; +import { + AccountInstrument, + CardInstrument, + CheckoutSelectors, + CustomerInitializeOptions, + CustomerRequestOptions, + Instrument, + PaymentInitializeOptions, + PaymentInstrument, + PaymentMethod, + PaymentRequestOptions, +} from '@bigcommerce/checkout-sdk'; import { memoizeOne } from '@bigcommerce/memoize'; import classNames from 'classnames'; import { find, noop, some } from 'lodash'; import React, { Component, ReactNode } from 'react'; import { ObjectSchema } from 'yup'; -import { withCheckout, CheckoutContextProps } from '../../checkout'; +import { PaymentFormValues } from '@bigcommerce/checkout/payment-integration-api'; + +import { CheckoutContextProps, withCheckout } from '../../checkout'; import { preventDefault } from '../../common/dom'; import { connectFormik, ConnectFormikProps } from '../../common/form'; import { MapToPropsFactory } from '../../common/hoc'; import { TranslatedString } from '../../locale'; import { LoadingOverlay } from '../../ui/loading'; -import { isBankAccountInstrument, isCardInstrument, isInstrumentCardCodeRequiredSelector, isInstrumentCardNumberRequiredSelector, isInstrumentFeatureAvailable, AccountInstrumentFieldset, CardInstrumentFieldset } from '../storedInstrument'; -import withPayment, { WithPaymentProps } from '../withPayment'; +import { + AccountInstrumentFieldset, + CardInstrumentFieldset, + isBankAccountInstrument, + isCardInstrument, + isInstrumentCardCodeRequiredSelector, + isInstrumentCardNumberRequiredSelector, + isInstrumentFeatureAvailable, +} from '../storedInstrument'; import StoreInstrumentFieldset from '../StoreInstrumentFieldset'; +import withPayment, { WithPaymentProps } from '../withPayment'; import SignOutLink from './SignOutLink'; @@ -38,11 +58,17 @@ export interface HostedWidgetPaymentMethodProps { shouldRenderCustomInstrument?: boolean; storedCardValidationSchema?: ObjectSchema; renderCustomPaymentForm?(): React.ReactNode; - validateInstrument?(shouldShowNumberField: boolean, selectedInstrument?: CardInstrument): React.ReactNode; + validateInstrument?( + shouldShowNumberField: boolean, + selectedInstrument?: CardInstrument, + ): React.ReactNode; deinitializeCustomer?(options: CustomerRequestOptions): Promise; deinitializePayment(options: PaymentRequestOptions): Promise; initializeCustomer?(options: CustomerInitializeOptions): Promise; - initializePayment(options: PaymentInitializeOptions, selectedInstrument?: CardInstrument): Promise; + initializePayment( + options: PaymentInitializeOptions, + selectedInstrument?: CardInstrument, + ): Promise; onPaymentSelect?(): void; onSignOut?(): void; onSignOutError?(error: Error): void; @@ -69,9 +95,9 @@ interface HostedWidgetPaymentMethodState { class HostedWidgetPaymentMethod extends Component< HostedWidgetPaymentMethodProps & - WithCheckoutHostedWidgetPaymentMethodProps & - ConnectFormikProps & - WithPaymentProps + WithCheckoutHostedWidgetPaymentMethodProps & + ConnectFormikProps & + WithPaymentProps > { state: HostedWidgetPaymentMethodState = { isAddingNewCard: false, @@ -92,14 +118,19 @@ class HostedWidgetPaymentMethod extends Component< if (isInstrumentFeatureAvailableProp) { await loadInstruments(); } + await this.initializeMethod(); } catch (error) { onUnhandledError(error); } } - async componentDidUpdate(prevProps: Readonly, - prevState: Readonly): Promise { + async componentDidUpdate( + prevProps: Readonly< + HostedWidgetPaymentMethodProps & WithCheckoutHostedWidgetPaymentMethodProps + >, + prevState: Readonly, + ): Promise { const { deinitializePayment = noop, instruments, @@ -108,14 +139,14 @@ class HostedWidgetPaymentMethod extends Component< setValidationSchema, } = this.props; - const { - selectedInstrumentId, - } = this.state; + const { selectedInstrumentId } = this.state; setValidationSchema(method, this.getValidationSchema()); - if (selectedInstrumentId !== prevState.selectedInstrumentId || - (prevProps.instruments.length > 0 && instruments.length === 0)) { + if ( + selectedInstrumentId !== prevState.selectedInstrumentId || + (prevProps.instruments.length > 0 && instruments.length === 0) + ) { try { await deinitializePayment({ gatewayId: method.gateway, @@ -169,62 +200,67 @@ class HostedWidgetPaymentMethod extends Component< shouldShow = true, } = this.props; - const { - isAddingNewCard, - selectedInstrumentId = this.getDefaultInstrumentId(), - } = this.state; + const { isAddingNewCard, selectedInstrumentId = this.getDefaultInstrumentId() } = + this.state; if (!shouldShow) { return null; } - const selectedInstrument = instruments.find(instrument => instrument.bigpayToken === selectedInstrumentId) || instruments[0]; + const selectedInstrument = + instruments.find((instrument) => instrument.bigpayToken === selectedInstrumentId) || + instruments[0]; - const shouldShowInstrumentFieldset = isInstrumentFeatureAvailableProp && instruments.length > 0; + const shouldShowInstrumentFieldset = + isInstrumentFeatureAvailableProp && instruments.length > 0; const shouldShowCreditCardFieldset = !shouldShowInstrumentFieldset || isAddingNewCard; const isLoading = (isInitializing || isLoadingInstruments) && !hideWidget; - const selectedAccountInstrument = this.getSelectedBankAccountInstrument(isAddingNewCard, selectedInstrument); - const shouldShowAccountInstrument = instruments[0] && isBankAccountInstrument(instruments[0]); + const selectedAccountInstrument = this.getSelectedBankAccountInstrument( + isAddingNewCard, + selectedInstrument, + ); + const shouldShowAccountInstrument = + instruments[0] && isBankAccountInstrument(instruments[0]); return ( - +
    - { shouldShowAccountInstrument && shouldShowInstrumentFieldset && } - - { !shouldShowAccountInstrument && shouldShowInstrumentFieldset && } - - { this.renderPaymentDescriptorIfAvailable() } - - { this.renderContainer(shouldShowCreditCardFieldset) } - - { isInstrumentFeatureAvailableProp && } - - { this.renderEditButtonIfAvailable() } - - { isSignedIn && } + {shouldShowAccountInstrument && shouldShowInstrumentFieldset && ( + + )} + + {!shouldShowAccountInstrument && shouldShowInstrumentFieldset && ( + + )} + + {this.renderPaymentDescriptorIfAvailable()} + + {this.renderContainer(shouldShowCreditCardFieldset)} + + {isInstrumentFeatureAvailableProp && ( + + )} + + {this.renderEditButtonIfAvailable()} + + {isSignedIn && }
    ); @@ -239,8 +275,12 @@ class HostedWidgetPaymentMethod extends Component< } = this.props; const { selectedInstrumentId = this.getDefaultInstrumentId() } = this.state; - const selectedInstrument = find(instruments, { bigpayToken: selectedInstrumentId }) as CardInstrument; - const shouldShowNumberField = selectedInstrument ? isInstrumentCardNumberRequiredProp(selectedInstrument) : false; + const selectedInstrument = find(instruments, { + bigpayToken: selectedInstrumentId, + }) as CardInstrument; + const shouldShowNumberField = selectedInstrument + ? isInstrumentCardNumberRequiredProp(selectedInstrument) + : false; if (hideVerificationFields) { return; @@ -249,8 +289,6 @@ class HostedWidgetPaymentMethod extends Component< if (validateInstrument) { return validateInstrument(shouldShowNumberField, selectedInstrument); } - - return; } renderContainer(shouldShowCreditCardFieldset: any): ReactNode { @@ -268,19 +306,26 @@ class HostedWidgetPaymentMethod extends Component< return (
    - { shouldRenderCustomInstrument && renderCustomPaymentForm && renderCustomPaymentForm() } + {shouldRenderCustomInstrument && + renderCustomPaymentForm && + renderCustomPaymentForm()}
    ); } @@ -312,8 +357,11 @@ class HostedWidgetPaymentMethod extends Component< return find(instruments, { bigpayToken: selectedInstrumentId }); } - private handleDeleteInstrument: (id: string) => void = id => { - const { instruments, formik: { setFieldValue } } = this.props; + private handleDeleteInstrument: (id: string) => void = (id) => { + const { + instruments, + formik: { setFieldValue }, + } = this.props; const { selectedInstrumentId } = this.state; if (instruments.length === 0) { @@ -332,8 +380,13 @@ class HostedWidgetPaymentMethod extends Component< } }; - private getSelectedBankAccountInstrument(isAddingNewCard: boolean, selectedInstrument: PaymentInstrument): AccountInstrument | undefined { - return !isAddingNewCard && selectedInstrument && isBankAccountInstrument(selectedInstrument) ? selectedInstrument : undefined; + private getSelectedBankAccountInstrument( + isAddingNewCard: boolean, + selectedInstrument: PaymentInstrument, + ): AccountInstrument | undefined { + return !isAddingNewCard && selectedInstrument && isBankAccountInstrument(selectedInstrument) + ? selectedInstrument + : undefined; } private renderEditButtonIfAvailable() { @@ -344,11 +397,11 @@ class HostedWidgetPaymentMethod extends Component< return (

    - { translatedString } + {translatedString}

    ); @@ -359,9 +412,7 @@ class HostedWidgetPaymentMethod extends Component< const { shouldShowDescriptor, paymentDescriptor } = this.props; if (shouldShowDescriptor && paymentDescriptor) { - return( -
    { paymentDescriptor }
    - ); + return
    {paymentDescriptor}
    ; } } @@ -396,12 +447,17 @@ class HostedWidgetPaymentMethod extends Component< setSubmit(method, null); - const selectedInstrument = instruments.find(instrument => instrument.bigpayToken === selectedInstrumentId) || instruments[0]; + const selectedInstrument = + instruments.find((instrument) => instrument.bigpayToken === selectedInstrumentId) || + instruments[0]; - return initializePayment({ - gatewayId: method.gateway, - methodId: method.id, - }, selectedInstrument); + return initializePayment( + { + gatewayId: method.gateway, + methodId: method.id, + }, + selectedInstrument, + ); } private getDefaultInstrumentId(): string | undefined { @@ -412,20 +468,14 @@ class HostedWidgetPaymentMethod extends Component< } const { instruments } = this.props; - const defaultInstrument = ( - instruments.find(instrument => instrument.defaultInstrument) || - instruments[0] - ); + const defaultInstrument = + instruments.find((instrument) => instrument.defaultInstrument) || instruments[0]; return defaultInstrument && defaultInstrument.bigpayToken; } private handleUseNewCard: () => void = async () => { - const { - deinitializePayment = noop, - initializePayment = noop, - method, - } = this.props; + const { deinitializePayment = noop, initializePayment = noop, method } = this.props; this.setState({ isAddingNewCard: true, @@ -443,7 +493,7 @@ class HostedWidgetPaymentMethod extends Component< }); }; - private handleSelectInstrument: (id: string) => void = id => { + private handleSelectInstrument: (id: string) => void = (id) => { this.setState({ isAddingNewCard: false, selectedInstrumentId: id, @@ -451,12 +501,7 @@ class HostedWidgetPaymentMethod extends Component< }; private handleSignOut: () => void = async () => { - const { - method, - onSignOut = noop, - onSignOutError = noop, - signOut, - } = this.props; + const { method, onSignOut = noop, onSignOutError = noop, signOut } = this.props; try { await signOut({ methodId: method.id }); @@ -472,28 +517,20 @@ const mapFromCheckoutProps: MapToPropsFactory< WithCheckoutHostedWidgetPaymentMethodProps, HostedWidgetPaymentMethodProps & ConnectFormikProps > = () => { - const filterInstruments = memoizeOne((instruments: PaymentInstrument[] = []) => instruments.filter( instrument => isCardInstrument(instrument) || isBankAccountInstrument(instrument))); + const filterInstruments = memoizeOne((instruments: PaymentInstrument[] = []) => + instruments.filter( + (instrument) => isCardInstrument(instrument) || isBankAccountInstrument(instrument), + ), + ); return (context, props) => { - - const { - isUsingMultiShipping = false, - method, - } = props; + const { isUsingMultiShipping = false, method } = props; const { checkoutService, checkoutState } = context; const { - data: { - getCheckout, - getConfig, - getCustomer, - getInstruments, - isPaymentDataRequired, - }, - statuses: { - isLoadingInstruments, - }, + data: { getCheckout, getConfig, getCustomer, getInstruments, isPaymentDataRequired }, + statuses: { isLoadingInstruments }, } = checkoutState; const checkout = getCheckout(); @@ -523,4 +560,6 @@ const mapFromCheckoutProps: MapToPropsFactory< }; }; -export default connectFormik(withPayment(withCheckout(mapFromCheckoutProps)(HostedWidgetPaymentMethod))); +export default connectFormik( + withPayment(withCheckout(mapFromCheckoutProps)(HostedWidgetPaymentMethod)), +); diff --git a/packages/core/src/app/payment/paymentMethod/KlarnaPaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/KlarnaPaymentMethod.spec.tsx index 4d1f8c522b..ebfe97e372 100644 --- a/packages/core/src/app/payment/paymentMethod/KlarnaPaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/KlarnaPaymentMethod.spec.tsx @@ -1,4 +1,9 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService, PaymentMethod } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, + PaymentMethod, +} from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import { Formik } from 'formik'; import { noop } from 'lodash'; @@ -9,7 +14,9 @@ import { getStoreConfig } from '../../config/config.mock'; import { createLocaleContext, LocaleContext, LocaleContextType } from '../../locale'; import { getPaymentMethod } from '../payment-methods.mock'; -import HostedWidgetPaymentMethod, { HostedWidgetPaymentMethodProps } from './HostedWidgetPaymentMethod'; +import HostedWidgetPaymentMethod, { + HostedWidgetPaymentMethodProps, +} from './HostedWidgetPaymentMethod'; import { default as PaymentMethodComponent, PaymentMethodProps } from './PaymentMethod'; import PaymentMethodId from './PaymentMethodId'; @@ -32,23 +39,17 @@ describe('when using Klarna payment', () => { localeContext = createLocaleContext(getStoreConfig()); method = { ...getPaymentMethod(), id: PaymentMethodId.Klarna }; - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - jest.spyOn(checkoutService, 'deinitializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'deinitializePayment').mockResolvedValue(checkoutState); - jest.spyOn(checkoutService, 'initializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'initializePayment').mockResolvedValue(checkoutState); - PaymentMethodTest = props => ( - - - - + PaymentMethodTest = (props) => ( + + + + @@ -56,34 +57,38 @@ describe('when using Klarna payment', () => { }); it('renders as hosted widget method', () => { - const container = mount(); - const component: ReactWrapper = container.find(HostedWidgetPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(HostedWidgetPaymentMethod); - expect(component.props()) - .toEqual(expect.objectContaining({ + expect(component.props()).toEqual( + expect.objectContaining({ containerId: 'paymentWidget', deinitializePayment: expect.any(Function), initializePayment: expect.any(Function), method, - })); + }), + ); }); it('initializes method with required config', () => { - const container = mount(); - const component: ReactWrapper = container.find(HostedWidgetPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(HostedWidgetPaymentMethod); component.prop('initializePayment')({ methodId: method.id, gatewayId: method.gateway, }); - expect(checkoutService.initializePayment) - .toHaveBeenCalledWith(expect.objectContaining({ + expect(checkoutService.initializePayment).toHaveBeenCalledWith( + expect.objectContaining({ methodId: method.id, gatewayId: method.gateway, [method.id]: { container: '#paymentWidget', }, - })); + }), + ); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/KlarnaPaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/KlarnaPaymentMethod.tsx index 3b08f31023..a543f96f1a 100644 --- a/packages/core/src/app/payment/paymentMethod/KlarnaPaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/KlarnaPaymentMethod.tsx @@ -1,7 +1,9 @@ -import React, { useCallback, FunctionComponent } from 'react'; +import React, { FunctionComponent, useCallback } from 'react'; import { Omit } from 'utility-types'; -import HostedWidgetPaymentMethod, { HostedWidgetPaymentMethodProps } from './HostedWidgetPaymentMethod'; +import HostedWidgetPaymentMethod, { + HostedWidgetPaymentMethodProps, +} from './HostedWidgetPaymentMethod'; export type KlarnaPaymentMethodProps = Omit; @@ -9,18 +11,24 @@ const KlarnaPaymentMethod: FunctionComponent = ({ initializePayment, ...rest }) => { - const initializeKlarnaPayment = useCallback(options => initializePayment({ - ...options, - klarna: { - container: '#paymentWidget', - }, - }), [initializePayment]); + const initializeKlarnaPayment = useCallback( + (options) => + initializePayment({ + ...options, + klarna: { + container: '#paymentWidget', + }, + }), + [initializePayment], + ); - return ; + return ( + + ); }; export default KlarnaPaymentMethod; diff --git a/packages/core/src/app/payment/paymentMethod/KlarnaV2PaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/KlarnaV2PaymentMethod.spec.tsx index 884dd503dc..cff62eaa80 100644 --- a/packages/core/src/app/payment/paymentMethod/KlarnaV2PaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/KlarnaV2PaymentMethod.spec.tsx @@ -1,4 +1,9 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService, PaymentMethod } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, + PaymentMethod, +} from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import { Formik } from 'formik'; import { noop } from 'lodash'; @@ -9,9 +14,11 @@ import { getStoreConfig } from '../../config/config.mock'; import { createLocaleContext, LocaleContext, LocaleContextType } from '../../locale'; import { getPaymentMethod } from '../payment-methods.mock'; -import PaymentMethodId from './PaymentMethodId'; -import HostedWidgetPaymentMethod, { HostedWidgetPaymentMethodProps } from './HostedWidgetPaymentMethod'; +import HostedWidgetPaymentMethod, { + HostedWidgetPaymentMethodProps, +} from './HostedWidgetPaymentMethod'; import { default as PaymentMethodComponent, PaymentMethodProps } from './PaymentMethod'; +import PaymentMethodId from './PaymentMethodId'; describe('when using Klarna payment', () => { let method: PaymentMethod; @@ -32,23 +39,17 @@ describe('when using Klarna payment', () => { localeContext = createLocaleContext(getStoreConfig()); method = { ...getPaymentMethod(), id: 'pay_now', gateway: PaymentMethodId.Klarna }; - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - jest.spyOn(checkoutService, 'deinitializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'deinitializePayment').mockResolvedValue(checkoutState); - jest.spyOn(checkoutService, 'initializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'initializePayment').mockResolvedValue(checkoutState); - PaymentMethodTest = props => ( - - - - + PaymentMethodTest = (props) => ( + + + + @@ -56,34 +57,38 @@ describe('when using Klarna payment', () => { }); it('renders as hosted widget method', () => { - const container = mount(); - const component: ReactWrapper = container.find(HostedWidgetPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(HostedWidgetPaymentMethod); - expect(component.props()) - .toEqual(expect.objectContaining({ - containerId: `${ method.id }Widget`, + expect(component.props()).toEqual( + expect.objectContaining({ + containerId: `${method.id}Widget`, deinitializePayment: expect.any(Function), initializePayment: expect.any(Function), method, - })); + }), + ); }); it('initializes method with required config', () => { - const container = mount(); - const component: ReactWrapper = container.find(HostedWidgetPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(HostedWidgetPaymentMethod); component.prop('initializePayment')({ methodId: method.id, gatewayId: method.gateway, }); - expect(checkoutService.initializePayment) - .toHaveBeenCalledWith(expect.objectContaining({ + expect(checkoutService.initializePayment).toHaveBeenCalledWith( + expect.objectContaining({ methodId: method.id, gatewayId: method.gateway, - [`${ method.gateway }v2`]: { - container: `#${ method.id }Widget`, + [`${method.gateway}v2`]: { + container: `#${method.id}Widget`, }, - })); + }), + ); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/KlarnaV2PaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/KlarnaV2PaymentMethod.tsx index bc21e4d044..7b1254ea3b 100644 --- a/packages/core/src/app/payment/paymentMethod/KlarnaV2PaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/KlarnaV2PaymentMethod.tsx @@ -1,7 +1,9 @@ -import React, { useCallback, FunctionComponent } from 'react'; +import React, { FunctionComponent, useCallback } from 'react'; import { Omit } from 'utility-types'; -import HostedWidgetPaymentMethod, { HostedWidgetPaymentMethodProps } from './HostedWidgetPaymentMethod'; +import HostedWidgetPaymentMethod, { + HostedWidgetPaymentMethodProps, +} from './HostedWidgetPaymentMethod'; export type KlarnaPaymentMethodProps = Omit; @@ -9,20 +11,24 @@ const KlarnaV2PaymentMethod: FunctionComponent = ({ initializePayment, ...rest }) => { - const initializeKlarnaV2Payment = useCallback(options => initializePayment( - { - ...options, - klarnav2: { - container: `#${options.methodId}Widget`, - }, - } - ), [initializePayment]); + const initializeKlarnaV2Payment = useCallback( + (options) => + initializePayment({ + ...options, + klarnav2: { + container: `#${options.methodId}Widget`, + }, + }), + [initializePayment], + ); - return ; + return ( + + ); }; export default KlarnaV2PaymentMethod; diff --git a/packages/core/src/app/payment/paymentMethod/MasterpassPaymentMethod.spec.tsx b/packages/core/src/app/payment/paymentMethod/MasterpassPaymentMethod.spec.tsx index b1e6236ac6..9c3090aa18 100644 --- a/packages/core/src/app/payment/paymentMethod/MasterpassPaymentMethod.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/MasterpassPaymentMethod.spec.tsx @@ -1,4 +1,9 @@ -import { createCheckoutService, CheckoutSelectors, CheckoutService, PaymentMethod } from '@bigcommerce/checkout-sdk'; +import { + CheckoutSelectors, + CheckoutService, + createCheckoutService, + PaymentMethod, +} from '@bigcommerce/checkout-sdk'; import { mount, ReactWrapper } from 'enzyme'; import { Formik } from 'formik'; import { noop } from 'lodash'; @@ -12,7 +17,9 @@ import { getPaymentMethod } from '../payment-methods.mock'; import { default as PaymentMethodComponent, PaymentMethodProps } from './PaymentMethod'; import PaymentMethodId from './PaymentMethodId'; import PaymentMethodType from './PaymentMethodType'; -import WalletButtonPaymentMethod, { WalletButtonPaymentMethodProps } from './WalletButtonPaymentMethod'; +import WalletButtonPaymentMethod, { + WalletButtonPaymentMethodProps, +} from './WalletButtonPaymentMethod'; describe('when using Masterpass payment', () => { let method: PaymentMethod; @@ -41,23 +48,17 @@ describe('when using Masterpass payment', () => { }, }; - jest.spyOn(checkoutState.data, 'getConfig') - .mockReturnValue(getStoreConfig()); + jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig()); - jest.spyOn(checkoutService, 'deinitializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'deinitializePayment').mockResolvedValue(checkoutState); - jest.spyOn(checkoutService, 'initializePayment') - .mockResolvedValue(checkoutState); + jest.spyOn(checkoutService, 'initializePayment').mockResolvedValue(checkoutState); - PaymentMethodTest = props => ( - - - - + PaymentMethodTest = (props) => ( + + + + @@ -65,33 +66,36 @@ describe('when using Masterpass payment', () => { }); it('renders as wallet button method', () => { - const container = mount(); + const container = mount(); - expect(container.find(WalletButtonPaymentMethod).props()) - .toEqual(expect.objectContaining({ + expect(container.find(WalletButtonPaymentMethod).props()).toEqual( + expect.objectContaining({ buttonId: 'walletButton', deinitializePayment: expect.any(Function), initializePayment: expect.any(Function), method, - })); + }), + ); }); it('initializes method with required config', () => { - const container = mount(); - const component: ReactWrapper = container.find(WalletButtonPaymentMethod); + const container = mount(); + const component: ReactWrapper = + container.find(WalletButtonPaymentMethod); component.prop('initializePayment')({ methodId: method.id, gatewayId: method.gateway, }); - expect(checkoutService.initializePayment) - .toHaveBeenCalledWith(expect.objectContaining({ + expect(checkoutService.initializePayment).toHaveBeenCalledWith( + expect.objectContaining({ methodId: method.id, gatewayId: method.gateway, [method.id]: { walletButton: 'walletButton', }, - })); + }), + ); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/MasterpassPaymentMethod.tsx b/packages/core/src/app/payment/paymentMethod/MasterpassPaymentMethod.tsx index 77a6b9637b..00e5e34686 100644 --- a/packages/core/src/app/payment/paymentMethod/MasterpassPaymentMethod.tsx +++ b/packages/core/src/app/payment/paymentMethod/MasterpassPaymentMethod.tsx @@ -1,11 +1,13 @@ import { PaymentInitializeOptions } from '@bigcommerce/checkout-sdk'; -import React, { useCallback, useMemo, FunctionComponent } from 'react'; +import React, { FunctionComponent, useCallback, useMemo } from 'react'; import { Omit } from 'utility-types'; -import { withCheckout, CheckoutContextProps } from '../../checkout'; +import { CheckoutContextProps, withCheckout } from '../../checkout'; import { masterpassFormatLocale, withLanguage, WithLanguageProps } from '../../locale'; -import WalletButtonPaymentMethod, { WalletButtonPaymentMethodProps } from './WalletButtonPaymentMethod'; +import WalletButtonPaymentMethod, { + WalletButtonPaymentMethodProps, +} from './WalletButtonPaymentMethod'; export type MasterpassPaymentMethodProps = Omit; @@ -13,44 +15,58 @@ interface WithCheckoutMasterpassProps { storeLanguage: string; } -const MasterpassPaymentMethod: FunctionComponent = ({ - initializePayment, - language, - storeLanguage, - ...rest -}) => { - const initializeMasterpassPayment = useCallback((options: PaymentInitializeOptions) => initializePayment({ - ...options, - masterpass: { - walletButton: 'walletButton', - }, - }), [initializePayment]); +const MasterpassPaymentMethod: FunctionComponent< + MasterpassPaymentMethodProps & WithLanguageProps & WithCheckoutMasterpassProps +> = ({ initializePayment, language, storeLanguage, ...rest }) => { + const initializeMasterpassPayment = useCallback( + (options: PaymentInitializeOptions) => + initializePayment({ + ...options, + masterpass: { + walletButton: 'walletButton', + }, + }), + [initializePayment], + ); - const { config: { testMode }, initializationData: { checkoutId, isMasterpassSrcEnabled } } = rest.method; + const { + config: { testMode }, + initializationData: { checkoutId, isMasterpassSrcEnabled }, + } = rest.method; const locale = masterpassFormatLocale(storeLanguage); - const signInButtonLabel = useMemo(() => ( - { - ), [checkoutId, language, locale, testMode, isMasterpassSrcEnabled]); + const signInButtonLabel = useMemo( + () => ( + {language.translate('payment.masterpass_name_text')} + ), + [checkoutId, language, locale, testMode, isMasterpassSrcEnabled], + ); - return ; + return ( + + ); }; -function mapFromCheckoutProps( - { checkoutState }: CheckoutContextProps) { - const { data: { getConfig } } = checkoutState; +function mapFromCheckoutProps({ checkoutState }: CheckoutContextProps) { + const { + data: { getConfig }, + } = checkoutState; const config = getConfig(); if (!config) { @@ -61,4 +77,5 @@ function mapFromCheckoutProps( storeLanguage: config.storeProfile.storeLanguage, }; } + export default withCheckout(mapFromCheckoutProps)(withLanguage(MasterpassPaymentMethod)); diff --git a/packages/core/src/app/payment/paymentMethod/MollieAPMCustomForm.spec.tsx b/packages/core/src/app/payment/paymentMethod/MollieAPMCustomForm.spec.tsx index 27d8df59a6..2a8574317f 100644 --- a/packages/core/src/app/payment/paymentMethod/MollieAPMCustomForm.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/MollieAPMCustomForm.spec.tsx @@ -8,7 +8,12 @@ import { getStoreConfig } from '../../config/config.mock'; import { createLocaleContext, LocaleContext, LocaleContextType } from '../../locale'; import { DropdownTrigger } from '../../ui/dropdown'; -import MollieAPMCustomForm, { Issuer, IssuerSelectButton, MollieCustomCardFormProps, OptionButton } from './MollieAPMCustomForm'; +import MollieAPMCustomForm, { + Issuer, + IssuerSelectButton, + MollieCustomCardFormProps, + OptionButton, +} from './MollieAPMCustomForm'; describe('MollieAPMCustomForm', () => { let localeContext: LocaleContextType; @@ -38,53 +43,59 @@ describe('MollieAPMCustomForm', () => { ], }, }; - let MollieAPMCustomFormTest: FunctionComponent ; + let MollieAPMCustomFormTest: FunctionComponent; beforeEach(() => { localeContext = createLocaleContext(getStoreConfig()); MollieAPMCustomFormTest = (props: MollieCustomCardFormProps) => ( - - - + + + ); }); it('should empty render', () => { - const container = mount(); + const container = mount( + , + ); expect(container.isEmptyRender()).toBe(true); }); it('should render MollieAPMCustomForm', () => { - const container = mount(); + const container = mount(); expect(container.find(DropdownTrigger)).toHaveLength(1); }); it('should change IssuerSelectButton Value when option is selected', () => { - const container = mount(); + const container = mount(); - expect(container.find(IssuerSelectButton).props()).toEqual(expect.objectContaining({ - selectedIssuer: { - id: '', - image: { - size1x: '', + expect(container.find(IssuerSelectButton).props()).toEqual( + expect.objectContaining({ + selectedIssuer: { + id: '', + image: { + size1x: '', + }, + name: 'Select your bank', }, - name: 'Select your bank', - }, - })); + }), + ); container.find(DropdownTrigger).simulate('click'); - container.find(OptionButton).at(0).simulate('click', { currentTarget: { dataset: { id: 'kbc'}}}); + container + .find(OptionButton) + .at(0) + .simulate('click', { currentTarget: { dataset: { id: 'kbc' } } }); - expect(container.find(IssuerSelectButton).props()).toEqual(expect.objectContaining({ - selectedIssuer: issuer, - })); + expect(container.find(IssuerSelectButton).props()).toEqual( + expect.objectContaining({ + selectedIssuer: issuer, + }), + ); }); }); diff --git a/packages/core/src/app/payment/paymentMethod/MollieAPMCustomForm.tsx b/packages/core/src/app/payment/paymentMethod/MollieAPMCustomForm.tsx index e54d13ab7e..6d3b1e881f 100644 --- a/packages/core/src/app/payment/paymentMethod/MollieAPMCustomForm.tsx +++ b/packages/core/src/app/payment/paymentMethod/MollieAPMCustomForm.tsx @@ -1,6 +1,6 @@ import { PaymentMethod } from '@bigcommerce/checkout-sdk'; import { FieldProps } from 'formik'; -import React, { useCallback, useEffect, useState, FunctionComponent, SyntheticEvent } from 'react'; +import React, { FunctionComponent, SyntheticEvent, useCallback, useEffect, useState } from 'react'; import { preventDefault } from '../../common/dom'; import { withLanguage, WithLanguageProps } from '../../locale'; @@ -33,18 +33,28 @@ interface OptionButtonProps { onClick?(event: SyntheticEvent): void; } -const MollieAPMCustomForm: FunctionComponent = ({ method, language }) => { +const MollieAPMCustomForm: FunctionComponent = ({ + method, + language, +}) => { const issuers: Issuer[] = method.initializationData?.paymentMethodsResponse; - const [ selectedIssuer, setSelectedIssuer ] = useState({ name: language.translate('payment.select_your_bank') , id: '', image: { size1x: '' } }); - const render = useCallback((props: FieldProps) => , [ selectedIssuer ]); + const [selectedIssuer, setSelectedIssuer] = useState({ + name: language.translate('payment.select_your_bank'), + id: '', + image: { size1x: '' }, + }); + const render = useCallback( + (props: FieldProps) => , + [selectedIssuer], + ); if (!issuers || issuers.length === 0) { return <>; } const handleClick = ({ currentTarget }: SyntheticEvent) => { - const _selectedIssuer = issuers.find(({ id }) => id === currentTarget?.dataset.id); + const _selectedIssuer = issuers.find(({ id }) => id === currentTarget.dataset.id); if (!_selectedIssuer) { return; @@ -58,27 +68,30 @@ const MollieAPMCustomForm: FunctionComponent - { issuers.map(issuer => -
  • - + {issuers.map((issuer) => ( +
  • +
  • - ) } + ))}
); - return (<> - - - - - ); + return ( + <> + + + + + + ); }; -export const HiddenInput: FunctionComponent = ({ field: { value, ...restField }, form, selectedIssuer}) => { - const Input = useCallback(() => , [restField]); +export const HiddenInput: FunctionComponent = ({ + field: { value, ...restField }, + form, + selectedIssuer, +}) => { + const Input = useCallback(() => , [restField]); useEffect(() => { if (value === selectedIssuer) { @@ -96,9 +109,9 @@ export const IssuerSelectButton: FunctionComponent = ({ selec className="instrumentSelect instrumentSelect-card button dropdown-button dropdown-toogle--select" href="#" id="issuerToggle" - onClick={ preventDefault() } + onClick={preventDefault()} > - { selectedIssuer.name } + {selectedIssuer.name} ); @@ -106,17 +119,9 @@ export const OptionButton: FunctionComponent = ({ issuer, ... const { name, image, id } = issuer; return ( - - - { + + + {name} ); }; diff --git a/packages/core/src/app/payment/paymentMethod/MollieCustomCardForm.spec.tsx b/packages/core/src/app/payment/paymentMethod/MollieCustomCardForm.spec.tsx index a5e8f5f6ac..c81fec0739 100644 --- a/packages/core/src/app/payment/paymentMethod/MollieCustomCardForm.spec.tsx +++ b/packages/core/src/app/payment/paymentMethod/MollieCustomCardForm.spec.tsx @@ -41,21 +41,20 @@ describe('MollieCustomForm', () => { containerId: 'cholder_containerId', }, }; - let MollieCustomFormTest: FunctionComponent ; + let MollieCustomFormTest: FunctionComponent; beforeEach(() => { MollieCustomFormTest = (props: MollieCustomCardFormProps) => ( - - + + ); }); it('should render cc options', () => { - const container = mount(); + const container = mount( + , + ); expect(container.find('[id="cnumber_containerId"]')).toHaveLength(1); expect(container.find('[id="expiry_containerId"]')).toHaveLength(1); @@ -64,13 +63,21 @@ describe('MollieCustomForm', () => { }); it('should empty render', () => { - const container = mount(); + const container = mount( + , + ); expect(container.isEmptyRender()).toBe(true); }); it('should render MollieAPMCustomForm', () => { - const container = mount(); + const container = mount( + , + ); expect(container.find(MollieAPMCustomForm)).toHaveLength(1); }); diff --git a/packages/core/src/app/payment/paymentMethod/MollieCustomCardForm.tsx b/packages/core/src/app/payment/paymentMethod/MollieCustomCardForm.tsx index ae3e598de8..e5081ada41 100644 --- a/packages/core/src/app/payment/paymentMethod/MollieCustomCardForm.tsx +++ b/packages/core/src/app/payment/paymentMethod/MollieCustomCardForm.tsx @@ -28,89 +28,73 @@ export interface MollieCustomCardFormProps { method: PaymentMethod; } -const MollieCustomCardForm: React.FunctionComponent = ({ options, isCreditCard, method }) => ( - !isCreditCard ? : -
-
- -
-
-
- -
-
-
-