feat: add medusa-react (#913)

This commit is contained in:
Zakaria El Asri
2021-12-14 19:09:36 +01:00
committed by GitHub
parent 40f6e88875
commit d0d8dd7bf6
97 changed files with 22822 additions and 7595 deletions
+18 -15
View File
@@ -1,22 +1,25 @@
const path = require(`path`);
const glob = require(`glob`);
const fs = require(`fs`);
const path = require(`path`)
const glob = require(`glob`)
const fs = require(`fs`)
const pkgs = glob
.sync(`./packages/*`)
.map((p) => p.replace(/^\./, `<rootDir>`));
const pkgs = glob.sync(`./packages/*`).map((p) => p.replace(/^\./, `<rootDir>`))
const reMedusa = /medusa$/;
const medusaDir = pkgs.find((p) => reMedusa.exec(p));
const medusaBuildDirs = [`dist`].map((dir) => path.join(medusaDir, dir));
const reMedusa = /medusa$/
const medusaDir = pkgs.find((p) => reMedusa.exec(p))
const medusaBuildDirs = [`dist`].map((dir) => path.join(medusaDir, dir))
const builtTestsDirs = pkgs
.filter((p) => fs.existsSync(path.join(p, `src`)))
.map((p) => path.join(p, `__tests__`));
const distDirs = pkgs.map((p) => path.join(p, `dist`));
const ignoreDirs = [].concat(medusaBuildDirs, builtTestsDirs, distDirs);
.map((p) => path.join(p, `__tests__`))
const distDirs = pkgs.map((p) => path.join(p, `dist`))
const ignoreDirs = [].concat(
medusaBuildDirs,
builtTestsDirs,
distDirs,
"<rootDir>/packages/medusa-react/*"
)
const coverageDirs = pkgs.map((p) => path.join(p, `src/**/*.js`));
const useCoverage = !!process.env.GENERATE_JEST_REPORT;
const coverageDirs = pkgs.map((p) => path.join(p, `src/**/*.js`))
const useCoverage = !!process.env.GENERATE_JEST_REPORT
module.exports = {
notify: true,
@@ -46,4 +49,4 @@ module.exports = {
testEnvironment: `node`,
moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`],
// setupFiles: [`<rootDir>/.jestSetup.js`],
};
}
+1
View File
@@ -35,6 +35,7 @@
"pg-god": "^1.0.11",
"prettier": "^2.1.1",
"resolve-cwd": "^3.0.0",
"ts-jest": "^27.1.1",
"typeorm": "^0.2.31"
},
"lint-staged": {
+2 -105
View File
@@ -1300,13 +1300,6 @@ ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ansi-align@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb"
integrity sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==
dependencies:
string-width "^3.0.0"
ansi-escapes@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
@@ -1473,13 +1466,6 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
axios-retry@^3.1.9:
version "3.1.9"
resolved "https://registry.yarnpkg.com/axios-retry/-/axios-retry-3.1.9.tgz#6c30fc9aeb4519aebaec758b90ef56fa03fe72e8"
integrity sha512-NFCoNIHq8lYkJa6ku4m+V1837TP6lCa7n79Iuf8/AqATAHYB0ISaAS1eyIenDOfHOLtym34W65Sjke2xjg2fsA==
dependencies:
is-retry-allowed "^1.1.0"
axios@^0.21.1:
version "0.21.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.2.tgz#21297d5084b2aeeb422f5d38e7be4fbb82239017"
@@ -1603,20 +1589,6 @@ bl@^4.1.0:
inherits "^2.0.4"
readable-stream "^3.4.0"
boxen@^5.0.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.1.tgz#4faca6a437885add0bf8d99082e272d480814cd4"
integrity sha512-JtIQYts08AFAYGF4eSh3pUt3NQkYV/e75pRtQmAVTLNWR/1L7Bsswxlgzgk8nmLEM+gFszsIlA9BgD3XnSqp3g==
dependencies:
ansi-align "^3.0.0"
camelcase "^6.2.0"
chalk "^4.1.0"
cli-boxes "^2.2.1"
string-width "^4.2.2"
type-fest "^0.20.2"
widest-line "^3.1.0"
wrap-ansi "^7.0.0"
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@@ -1721,11 +1693,6 @@ camelcase@^5.0.0, camelcase@^5.3.1:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
camelcase@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809"
integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
caniuse-lite@^1.0.30001219:
version "1.0.30001236"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001236.tgz#0a80de4cdf62e1770bb46a30d884fc8d633e3958"
@@ -1813,11 +1780,6 @@ ci-info@^2.0.0:
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
ci-info@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.2.0.tgz#2876cb948a498797b5236f0095bc057d0dca38b6"
integrity sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A==
class-utils@^0.3.5:
version "0.3.6"
resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
@@ -1835,11 +1797,6 @@ clean-stack@^3.0.0:
dependencies:
escape-string-regexp "4.0.0"
cli-boxes@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f"
integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==
cli-cursor@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
@@ -2246,11 +2203,6 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
dom-walk@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84"
integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==
domexception@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
@@ -2875,14 +2827,6 @@ glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
once "^1.3.0"
path-is-absolute "^1.0.0"
global@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406"
integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==
dependencies:
min-document "^2.19.0"
process "^0.11.10"
globals@^11.1.0:
version "11.12.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
@@ -3220,7 +3164,7 @@ is-docker@^2.0.0:
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.0.0.tgz#2cb0df0e75e2d064fe1864c37cdeacb7b2dcf25b"
integrity sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==
is-docker@^2.1.1, is-docker@^2.2.1:
is-docker@^2.1.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
@@ -3319,11 +3263,6 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
is-retry-allowed@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4"
integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==
is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
@@ -4104,21 +4043,6 @@ medusa-core-utils@^0.1.27:
"@hapi/joi" "^16.1.8"
joi-objectid "^3.0.1"
medusa-telemetry@^0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/medusa-telemetry/-/medusa-telemetry-0.0.5.tgz#d7d08fca5cbecc0e853b4e0406194a92c5206caa"
integrity sha512-h7hP5Lc33OkFhMcvfrPcwINzMOuPoG8Vn8O6niKGFxF9RmmQnJgaAG1J43/Eq9ZWBrWi0n42XBttibKwCMViHw==
dependencies:
axios "^0.21.1"
axios-retry "^3.1.9"
boxen "^5.0.1"
ci-info "^3.2.0"
configstore "5.0.1"
global "^4.4.0"
is-docker "^2.2.1"
remove-trailing-slash "^0.1.1"
uuid "^8.3.2"
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@@ -4181,13 +4105,6 @@ mimic-fn@^2.1.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
min-document@^2.19.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
integrity sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=
dependencies:
dom-walk "^0.1.0"
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
@@ -4727,11 +4644,6 @@ process-nextick-args@~2.0.0:
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
process@^0.11.10:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
progress@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
@@ -4925,11 +4837,6 @@ remove-trailing-separator@^1.0.1:
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
remove-trailing-slash@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/remove-trailing-slash/-/remove-trailing-slash-0.1.1.tgz#be2285a59f39c74d1bce4f825950061915e3780d"
integrity sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA==
repeat-element@^1.1.2:
version "1.1.3"
resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
@@ -5417,7 +5324,7 @@ string-width@^3.0.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.1.0"
string-width@^4.0.0, string-width@^4.2.2:
string-width@^4.0.0:
version "4.2.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5"
integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==
@@ -5701,11 +5608,6 @@ type-fest@^0.11.0:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1"
integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==
type-fest@^0.20.2:
version "0.20.2"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
type-fest@^0.21.3:
version "0.21.3"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
@@ -5831,11 +5733,6 @@ uuid@^3.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
v8-compile-cache@^2.0.3:
version "2.1.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e"
+1 -1
View File
@@ -16,7 +16,7 @@
"license": "MIT",
"dependencies": {
"@medusajs/medusa": "^1.1.59",
"axios": "^0.21.0",
"axios": "^0.24.0",
"retry-axios": "^2.6.0"
},
"repository": {
+2 -2
View File
@@ -1,4 +1,4 @@
import axios, { AxiosError, AxiosInstance } from "axios"
import axios, { AxiosError, AxiosInstance, AxiosRequestHeaders } from "axios"
import * as rax from "retry-axios"
import { v4 as uuidv4 } from "uuid"
@@ -108,7 +108,7 @@ class Client {
userHeaders: RequestOptions,
method: RequestMethod,
path: string
): object {
): AxiosRequestHeaders {
let defaultHeaders: object = {
Accept: "application/json",
"Content-Type": "application/json",
+10 -2
View File
@@ -1,7 +1,15 @@
import { AxiosResponse } from "axios"
export interface HTTPResponse {
status: number
statusText: string
headers: Record<string, string> & {
"set-cookie"?: string[]
}
config: any
request?: any
}
export type Response<T> = T & {
response: Omit<AxiosResponse<T>, "data">
response: HTTPResponse
}
export type ResponsePromise<T = any> = Promise<Response<T>>
+13 -1
View File
@@ -1186,13 +1186,20 @@ axios-retry@^3.1.9:
"@babel/runtime" "^7.15.4"
is-retry-allowed "^2.2.0"
axios@^0.21.0, axios@^0.21.1:
axios@^0.21.1:
version "0.21.4"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==
dependencies:
follow-redirects "^1.14.0"
axios@^0.24.0:
version "0.24.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6"
integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==
dependencies:
follow-redirects "^1.14.4"
babel-jest@^26.6.3:
version "26.6.3"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.6.3.tgz#d87d25cb0037577a0c89f82e5755c5d293c01056"
@@ -2741,6 +2748,11 @@ follow-redirects@^1.14.0:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.5.tgz#f09a5848981d3c772b5392309778523f8d85c381"
integrity sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==
follow-redirects@^1.14.4:
version "1.14.6"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.6.tgz#8cfb281bbc035b3c067d6cd975b0f6ade6e855cd"
integrity sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==
for-each@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
File diff suppressed because it is too large Load Diff
-81
View File
@@ -879,18 +879,6 @@
exec-sh "^0.3.2"
minimist "^1.2.0"
"@hapi/hoek@^9.0.0":
version "9.2.0"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.0.tgz#f3933a44e365864f4dad5db94158106d511e8131"
integrity sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug==
"@hapi/topo@^5.0.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012"
integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==
dependencies:
"@hapi/hoek" "^9.0.0"
"@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@@ -1083,23 +1071,6 @@
component-type "^1.2.1"
join-component "^1.1.0"
"@sideway/address@^4.1.0":
version "4.1.2"
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.2.tgz#811b84333a335739d3969cfc434736268170cad1"
integrity sha512-idTz8ibqWFrPU8kMirL0CoPH/A29XOzzAzpyN3zQ4kAWnzmNfFmRaoMNN6VI8ske5M73HZyhIaW4OuSFIdM4oA==
dependencies:
"@hapi/hoek" "^9.0.0"
"@sideway/formula@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
"@sideway/pinpoint@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df"
integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==
"@sinonjs/commons@^1.7.0":
version "1.8.0"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.0.tgz#c8d68821a854c555bba172f3b06959a0039b236d"
@@ -3026,11 +2997,6 @@ is-number@^3.0.0:
dependencies:
kind-of "^3.0.2"
is-number@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff"
integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
@@ -3521,22 +3487,6 @@ jest@^25.5.2:
import-local "^3.0.2"
jest-cli "^25.5.4"
joi-objectid@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/joi-objectid/-/joi-objectid-3.0.1.tgz#63ace7860f8e1a993a28d40c40ffd8eff01a3668"
integrity sha512-V/3hbTlGpvJ03Me6DJbdBI08hBTasFOmipsauOsxOSnsF1blxV537WTl1zPwbfcKle4AK0Ma4OPnzMH4LlvTpQ==
joi@^17.3.0:
version "17.4.0"
resolved "https://registry.yarnpkg.com/joi/-/joi-17.4.0.tgz#b5c2277c8519e016316e49ababd41a1908d9ef20"
integrity sha512-F4WiW2xaV6wc1jxete70Rw4V/VuMd6IN+a5ilZsxG4uYtUXWu2kq9W5P2dz30e7Gmw8RCbY/u/uk+dMPma9tAg==
dependencies:
"@hapi/hoek" "^9.0.0"
"@hapi/topo" "^5.0.0"
"@sideway/address" "^4.1.0"
"@sideway/formula" "^3.0.0"
"@sideway/pinpoint" "^2.0.0"
join-component@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/join-component/-/join-component-1.1.0.tgz#b8417b750661a392bee2c2537c68b2a9d4977cd5"
@@ -3776,11 +3726,6 @@ map-visit@^1.0.0:
dependencies:
object-visit "^1.0.0"
math-random@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c"
integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==
md5@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9"
@@ -3795,23 +3740,6 @@ media-typer@0.3.0:
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
medusa-core-utils@^1.1.16:
version "1.1.16"
resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-1.1.16.tgz#e7002d861aebf81dec7bd0654615eefbd55cb530"
integrity sha512-O176mtAILLbwahxJu2dAOZRnz9AzrX6Oa1NDhrtbBvaPuaFZlhxiajwZkP3KM58bGZ9feKfJ4mVuY2Mtsgj3YA==
dependencies:
joi "^17.3.0"
joi-objectid "^3.0.1"
medusa-test-utils@^1.1.19:
version "1.1.19"
resolved "https://registry.yarnpkg.com/medusa-test-utils/-/medusa-test-utils-1.1.19.tgz#f765b6ba39e0bfe6301423a9b39d4e7e3b1ed32b"
integrity sha512-GkGWOUQsrNTm7tv2P7fG9f651+C05cLpy+nuPPSouIAkxpS0mDqeD1VSrHjFnpz1BPUsjwJGWFFAH5Jd8X5yvQ==
dependencies:
"@babel/plugin-transform-classes" "^7.9.5"
medusa-core-utils "^1.1.16"
randomatic "^3.1.1"
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
@@ -4362,15 +4290,6 @@ qs@~6.5.2:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
randomatic@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed"
integrity sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==
dependencies:
is-number "^4.0.0"
kind-of "^6.0.0"
math-random "^1.0.1"
range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
+12
View File
@@ -0,0 +1,12 @@
module.exports = {
"extends": [
"react-app",
"prettier/@typescript-eslint",
"plugin:prettier/recommended"
],
"settings": {
"react": {
"version": "detect"
}
}
}
+32
View File
@@ -0,0 +1,32 @@
name: CI
on: [push]
jobs:
build:
name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
node: ["16.x"]
os: [ubuntu-latest, windows-latest, macOS-latest]
steps:
- name: Checkout repo
uses: actions/checkout@v2
- name: Use Node ${{ matrix.node }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}
- name: Install deps and build (with cache)
uses: bahmutov/npm-install@v1
- name: Lint
run: yarn lint
- name: Test
run: yarn test --ci --coverage --maxWorkers=2
- name: Build
run: yarn build
+12
View File
@@ -0,0 +1,12 @@
name: size
on: [pull_request]
jobs:
size:
runs-on: ubuntu-latest
env:
CI_JOB_NUMBER: 1
steps:
- uses: actions/checkout@v1
- uses: andresz1/size-limit-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
+5
View File
@@ -0,0 +1,5 @@
*.log
.DS_Store
node_modules
.cache
dist
+12
View File
@@ -0,0 +1,12 @@
/lib
node_modules
.DS_store
.env*
/*.js
!index.js
test/
public/
.storybook/
mocks/
stories/
yarn.lock
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
stories: ['../stories/**/*.stories.@(ts|tsx|js|jsx)'],
addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
// https://storybook.js.org/docs/react/configure/typescript#mainjs-configuration
typescript: {
check: true, // type-check stories during Storybook build
}
};
@@ -0,0 +1,23 @@
import React from "react"
import { QueryClient } from "react-query"
import { MedusaProvider } from "../src"
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
staleTime: Infinity,
retry: 1,
},
},
})
const DefaultMedusaProvider = (props) => (
<MedusaProvider
queryClientProviderProps={{ client: queryClient }}
baseUrl={""}
{...props}
/>
)
export default DefaultMedusaProvider
@@ -0,0 +1,24 @@
import React from "react"
import DefaultMedusaProvider from "./medusa-context"
import { initialize, mswDecorator } from "msw-storybook-addon"
import { handlers } from "../mocks/handlers"
initialize()
// https://storybook.js.org/docs/react/writing-stories/parameters#global-parameters
export const parameters = {
// https://storybook.js.org/docs/react/essentials/actions#automatically-matching-args
actions: { argTypesRegex: "^on.*" },
msw: {
handlers,
},
}
export const decorators = [
mswDecorator,
(Story) => (
<DefaultMedusaProvider>
<Story />
</DefaultMedusaProvider>
),
]
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Zakaria S. El Asri
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+306
View File
@@ -0,0 +1,306 @@
# Medusa React
A React library providing a set of components, utilities, and hooks for interacting seamlessly with a Medusa backend and building custom React storefronts.
## Installation
The library uses [react-query](https://react-query.tanstack.com/overview) as a solution for server-side state management and lists the library as a peer dependency.
In order to install the package, run the following
```bash
npm install @medusajs/medusa-react react-query
# or
yarn add @medusajs/medusa-react react-query
```
## Quick Start
In order to use the hooks exposed by medusa-react, you will need to include the `MedusaProvider` somewhere up in your component tree. The `MedusaProvider` takes a `baseUrl` prop which should point to your Medusa server. Under the hood, `medusa-react` uses the `medusa-js` client library (built on top of axios) to interact with your server.
In addition, because medusa-react is built on top of react-query, you can pass an object representing react-query's [QueryClientProvider](https://react-query.tanstack.com/reference/QueryClientProvider#_top) props, which will be passed along by `MedusaProvider`.
```jsx
// App.tsx
import * as React from "react"
import { QueryClient } from "react-query"
import { MedusaProvider } from "../src"
import MyStorefront from "./my-storefront"
// Your react-query's query client config
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
staleTime: 30000,
retry: 1,
},
},
})
const App = () => {
return (
<MedusaProvider
queryClientProviderProps={{ client: queryClient }}
baseUrl="http://localhost:9000"
>
<MyStorefront />
</MedusaProvider>
)
}
export default App
```
The hooks exposed by `medusa-react` fall into two main categories: queries and mutations.
### Queries
[Queries](https://react-query.tanstack.com/guides/queries#_top) simply wrap around react-query's `useQuery` hook to fetch some data from your medusa server
```jsx
// ./my-storefront.tsx
import * as React from "react"
import { useProducts } from "@medusajs/medusa-react"
const MyStorefront = () => {
const { products, isLoading } = useProducts()
return isLoading ? (
<Spinner />
) : (
products.map((product) => <Product product={product} />)
)
}
```
In general, the queries will return everything react-query returns from [`useQuery`](https://react-query.tanstack.com/reference/useQuery#_top) except the `data` field, which will be flattened out. In addition, you can also access the HTTP response object returned from the `medusa-js` client including things like `status`, `headers`, etc.
So, in other words, we can express what the above query returns as the following:
```typescript
import { UseQueryResult } from "react-query"
// This is what a Medusa server returns when you hit the GET /store/products endpoint
type ProductsResponse = {
products: Product[]
limit: number
offset: number
}
// UseProductsQuery refers to what's returned by the useProducts hook
type UseProductsQuery = ProductsResponse &
Omit<UseQueryResult, "data"> & {
response: {
status: number
statusText: string
headers: Record<string, string> & {
"set-cookie"?: string[]
}
config: any
request?: any
}
}
// More generally ...
type QueryReturnType = APIResponse &
Omit<UseQueryResult, "data"> & {
response: {
status: number
statusText: string
headers: Record<string, string> & {
"set-cookie"?: string[]
}
config: any
request?: any
}
}
```
### Mutations
[Mutations](https://react-query.tanstack.com/guides/mutations#_top) wrap around react-query's `useMutation` to mutate data and perform server-side effects on your medusa server. If you are not entirely familiar with this idea of "mutations", creating a cart would be a mutation because it creates a cart in your server (and database). Mutations also have to be invoked imperatively, meaning that calling for the mutation to take action, you will have to call a `mutate()` function returned from mutation hooks.
```jsx
import * as React from "react"
import { useCreateCart } from "@medusajs/medusa-react"
const CreateCartButton = () => {
const createCart = useCreateCart()
const handleClick = () => {
createCart.mutate({}) // create an empty cart
}
return (
<Button isLoading={createCart.isLoading} onClick={handleClick}>
Create cart
</Button>
)
}
```
The mutation hooks will return exactly what react-query's [`useMutation`](https://react-query.tanstack.com/reference/useMutation#_top) returns. In addition, the options you pass in to the hooks will be passed along to `useMutation`.
### Utilities
A set of utility functions are also exposed from the library to make your life easier when dealing with displaying money amounts
#### `formatVariantPrice()`
- `formatVariantPrice(params: FormatVariantPriceParams): string`
```typescript
type FormatVariantPriceParams = {
variant: ProductVariantInfo
region: RegionInfo
includeTaxes?: boolean
minimumFractionDigits?: number
maximumFractionDigits?: number
locale?: string
}
type ProductVariantInfo = Pick<ProductVariant, "prices">
type RegionInfo = {
currency_code: string
tax_code: string
tax_rate: number
}
```
Given a variant and region, will return a string representing the localized amount (i.e: `$19.50`)
The behavior of minimumFractionDigits and maximumFractionDigits is the same as the one explained by MDN [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat). In fact, in order to convert the decimal amount, we use the browser's `Intl.NumberFormat` method.
#### `computeVariantPrice()`
- `computeVariantPrice(params: ComputeVariantPriceParams): number`
```typescript
type ComputeVariantPriceParams = {
variant: ProductVariantInfo
region: RegionInfo
includeTaxes?: boolean
}
```
Determines a variant's price based on the region provided. Returns a decimal number representing the amount.
#### `formatAmount()`
- `formatAmount(params: FormatAmountParams): string`
```typescript
type FormatAmountParams = {
amount: number
region: RegionInfo
includeTaxes?: boolean
minimumFractionDigits?: number
maximumFractionDigits?: number
locale?: string
}
```
Returns a localized string based on the input params representing the amount (i.e: "$10.99").
#### `computeAmount()`
- `computeAmount(params: ComputeAmountParams): number`
```typescript
type ComputeAmountParams = {
amount: number
region: RegionInfo
includeTaxes?: boolean
}
```
Takes an integer amount, a region, and includeTaxes boolean. Returns a decimal amount including (or excluding) taxes.
### Context Providers (Experimental)
In order to make building custom storefronts easier, we also expose a `SessionCartProvider` and a `CartProvider` . At first, the two sound very similar to each other, however, the main distinction between the two is that the `SessionCartProvider` never interacts with your medusa server.
The main goal behind the provider is to manage the state related to your users' cart experience. In other words, the provider keeps track of the items users add to their cart and help you interact with those items through a set of helpful methods like `addItem`, `updateQuantity`, `removeItem` , etc.
On the other hand the `CartProvider` makes use of some of the hooks already exposed by `medusa-react` to help you create a cart (on the medusa backend), start the checkout flow, authorize payment sessions, etc. It also manages one single global piece of state which represents a cart, exactly like the one created on your medusa backend.
You can think of a `sessionCart` as a purely client-side lightweight cart, in other words, just a javascript object living in your browser, whereas `cart` is the entity which you have stored in your database.
### SessionCart
The first step to using the `SessionCartProvider` is by inserting it somewhere up in your component tree.
```jsx
// App.tsx
import * as React from "react"
import { QueryClient } from "react-query"
import { MedusaProvider, SessionCartProvider } from "medusa-react"
import MyStorefront from "./my-storefront"
// Your react-query's query client config
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
staleTime: 30000,
retry: 1,
},
},
})
const App = () => {
return (
<MedusaProvider
queryClientProviderProps={{ client: queryClient }}
baseUrl="http://localhost:9000"
>
<SessionCartProvider>
<MyStorefront />
</SessionCartProvider>
</MedusaProvider>
)
}
export default App
```
### Cart
```jsx
// App.tsx
import * as React from "react"
import { QueryClient } from "react-query"
import { MedusaProvider, CartProvider } from "medusa-react"
import MyStorefront from "./my-storefront"
// Your react-query's query client config
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
staleTime: 30000,
retry: 1,
},
},
})
const App = () => {
return (
<MedusaProvider
queryClientProviderProps={{ client: queryClient }}
baseUrl="http://localhost:9000"
>
<CartProvider>
<MyStorefront />
</CartProvider>
</MedusaProvider>
)
}
export default App
```
+10
View File
@@ -0,0 +1,10 @@
module.exports = {
// [...]
globals: {
"ts-jest": {
diagnostics: false,
isolatedModules: true,
},
},
setupFilesAfterEnv: ["./jest.setup.js"],
}
+21
View File
@@ -0,0 +1,21 @@
const { server } = require("./mocks/server")
beforeAll(() => {
server.listen({
onUnhandledRequest: (req) => {
console.log("found an error")
console.log(req)
},
})
})
beforeEach(() => {
window.localStorage.clear()
})
afterEach(() => {
server.resetHandlers()
window.localStorage.clear()
})
afterAll(() => server.close())
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
import data from "./fixtures.json"
const resources = data["resources"]
type Resources = typeof resources
type ResourcesWithKey<Entity extends string, T> = {
[K in keyof T]: { [_ in Entity]: K } & T[K]
}
type KeyedResources = ResourcesWithKey<"entity", Resources>
export const fixtures = {
get<Entity extends keyof Resources>(
entity: Entity
): Omit<KeyedResources[Entity], "entity"> {
return resources[entity as string]
},
list<Entity extends keyof Resources>(
entity: Entity,
number = 2
): Omit<KeyedResources[Entity], "entity">[] {
return Array(number)
.fill(null)
.map((_) => fixtures.get(entity))
},
} as const
+424
View File
@@ -0,0 +1,424 @@
import { fixtures } from "./data/"
import { rest } from "msw"
export const handlers = [
rest.get("/store/products", (req, res, ctx) => {
const limit = parseInt(req.url.searchParams.get("limit") || "2")
const offset = parseInt(req.url.searchParams.get("offset") || "0")
return res(
ctx.status(200),
ctx.json({
products: fixtures.list("product", limit),
offset,
limit,
})
)
}),
rest.get("/store/products/:id", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
product: fixtures.get("product"),
})
)
}),
rest.get("/store/collections/", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
collections: fixtures.list("product_collection"),
})
)
}),
rest.get("/store/collections/:id", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
collection: fixtures.get("product_collection"),
})
)
}),
rest.get("/store/regions/", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
regions: fixtures.list("region"),
})
)
}),
rest.get("/store/regions/:id", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
region: fixtures.get("region"),
})
)
}),
rest.get("/store/gift-cards/:id", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
gift_card: fixtures.get("gift_card"),
})
)
}),
rest.get("/store/orders/:id", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
order: fixtures.get("order"),
})
)
}),
rest.get("/store/orders/cart/:id", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
order: fixtures.get("order"),
})
)
}),
rest.get("/store/orders/", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
orders: fixtures.get("order"),
})
)
}),
rest.get("/store/return-reasons/", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
return_reasons: fixtures.list("return_reason"),
})
)
}),
rest.get("/store/return-reasons/:id", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
return_reason: fixtures.get("return_reason"),
})
)
}),
rest.get("/store/shipping-options/:cart_id", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
shipping_options: fixtures.list("shipping_option"),
})
)
}),
rest.get("/store/shipping-options/", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
shipping_options: fixtures.list("shipping_option", 5),
})
)
}),
rest.get("/store/swaps/:cart_id", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
swap: fixtures.get("swap"),
})
)
}),
rest.get("/store/customers/me", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
customer: fixtures.get("customer"),
})
)
}),
rest.get("/store/customers/me/orders", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
orders: fixtures.list("order", 5),
limit: 5,
offset: 0,
})
)
}),
rest.post("/store/customers/", (req, res, ctx) => {
const body = req.body as Record<string, string>
const dummyCustomer = fixtures.get("customer")
const customer = {
...dummyCustomer,
...body,
}
return res(
ctx.status(200),
ctx.json({
customer,
})
)
}),
rest.post("/store/customers/me", (req, res, ctx) => {
const body = req.body as Record<string, string>
const dummyCustomer = fixtures.get("customer")
const customer = {
...dummyCustomer,
...body,
}
return res(
ctx.status(200),
ctx.json({
customer,
})
)
}),
rest.get("/store/carts/:id", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
cart: fixtures.get("cart"),
})
)
}),
rest.post("/store/returns/", (req, res, ctx) => {
const { items, ...body } = req.body as Record<string, any>
const ret = fixtures.get("return")
const item = ret.items[0]
ret.items = items.map((i) => ({ ...i, ...item }))
return res(
ctx.status(200),
ctx.json({
return: {
...ret,
...body,
},
})
)
}),
rest.post("/store/swaps/", (req, res, ctx) => {
const { additional_items, return_items, ...body } = req.body as Record<
string,
any
>
const swap = fixtures.get("swap")
const additional_item = swap.additional_items[0]
swap.additional_items = additional_items.map((i) => ({
...i,
...additional_item,
}))
const return_item = swap.return_order.items[0]
swap.return_order.items = return_items.map((i) => ({
...i,
...return_item,
}))
return res(
ctx.status(200),
ctx.json({
swap: {
...swap,
...body,
},
})
)
}),
rest.post("/store/carts/:id/line-items", (req, res, ctx) => {
const { id } = req.params
const { quantity, variant_id } = req.body as Record<string, any>
const item = fixtures.get("line_item")
return res(
ctx.status(200),
ctx.json({
cart: {
...fixtures.get("cart"),
id,
items: [
{
...item,
quantity,
variant_id,
},
],
},
})
)
}),
rest.post("/store/carts/:id/line-items/:line_id", (req, res, ctx) => {
const { id, line_id } = req.params
const { quantity } = req.body as Record<string, any>
const item = fixtures.get("line_item")
return res(
ctx.status(200),
ctx.json({
cart: {
...fixtures.get("cart"),
id,
items: [
{
...item,
id: line_id,
quantity,
},
],
},
})
)
}),
rest.delete("/store/carts/:id/line-items/:line_id", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
cart: fixtures.get("cart"),
})
)
}),
rest.post("/store/carts/", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
cart: fixtures.get("cart"),
})
)
}),
rest.post("/store/carts/:id", (req, res, ctx) => {
const { id } = req.params
const body = req.body as Record<string, any>
return res(
ctx.status(200),
ctx.json({
cart: {
...fixtures.get("cart"),
id,
...body,
},
})
)
}),
rest.post("/store/carts/:id/complete", (req, res, ctx) => {
const { id } = req.params
return res(
ctx.status(200),
ctx.json({
type: "order",
data: fixtures.get("order"),
})
)
}),
rest.post("/store/carts/:id/payment-sessions", (req, res, ctx) => {
const { id } = req.params
return res(
ctx.status(200),
ctx.json({
cart: {
...fixtures.get("cart"),
id,
},
})
)
}),
rest.post(
"/store/carts/:id/payment-sessions/:provider_id",
(req, res, ctx) => {
const { id } = req.params
return res(
ctx.status(200),
ctx.json({
cart: {
...fixtures.get("cart"),
id,
},
})
)
}
),
rest.post(
"/store/carts/:id/payment-sessions/:provider_id/refresh",
(req, res, ctx) => {
const { id } = req.params
return res(
ctx.status(200),
ctx.json({
cart: {
...fixtures.get("cart"),
id,
},
})
)
}
),
rest.post("/store/carts/:id/payment-session", (req, res, ctx) => {
const { id } = req.params
return res(
ctx.status(200),
ctx.json({
cart: {
...fixtures.get("cart"),
id,
},
})
)
}),
rest.delete(
"/store/carts/:id/payment-sessions/:provider_id",
(req, res, ctx) => {
const { id } = req.params
return res(
ctx.status(200),
ctx.json({
cart: {
...fixtures.get("cart"),
id,
},
})
)
}
),
rest.post("/store/carts/:id/shipping-methods", (req, res, ctx) => {
const { id } = req.params
return res(
ctx.status(200),
ctx.json({
cart: {
...fixtures.get("cart"),
id,
},
})
)
}),
]
+4
View File
@@ -0,0 +1,4 @@
import { setupServer } from "msw/node"
import { handlers } from "./handlers"
export const server = setupServer(...handlers)
+102
View File
@@ -0,0 +1,102 @@
{
"version": "0.1.0",
"license": "MIT",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=10"
},
"scripts": {
"start": "tsdx watch",
"build": "tsdx build",
"test": "tsdx test --passWithNoTests",
"lint": "tsdx lint",
"prepare": "tsdx build",
"size": "size-limit",
"analyze": "size-limit --why",
"storybook": "start-storybook -p 6006 -s public",
"build-storybook": "build-storybook"
},
"peerDependencies": {
"react": ">=16",
"react-query": ">= 3.29.0"
},
"husky": {
"hooks": {
"pre-commit": "tsdx lint"
}
},
"prettier": {
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
},
"name": "medusa-react",
"author": "Zakaria S. El Asri",
"module": "dist/medusa-react.esm.js",
"size-limit": [
{
"path": "dist/medusa-react.cjs.production.min.js",
"limit": "10 KB"
},
{
"path": "dist/medusa-react.esm.js",
"limit": "10 KB"
}
],
"devDependencies": {
"@babel/core": "^7.16.0",
"@size-limit/preset-small-lib": "^6.0.4",
"@storybook/addon-contexts": "^5.3.21",
"@storybook/addon-essentials": "^6.3.12",
"@storybook/addon-info": "^5.3.21",
"@storybook/addon-links": "^6.3.12",
"@storybook/addons": "^6.3.12",
"@storybook/react": "^6.3.12",
"@testing-library/react": "^12.1.2",
"@testing-library/react-hooks": "^7.0.2",
"@types/lodash": "^4.14.177",
"@types/react": "^17.0.33",
"@types/react-dom": "^17.0.10",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"axios": "^0.24.0",
"babel-loader": "^8.2.3",
"eslint-config-react-app": "^6.0.0",
"eslint-plugin-prettier": "3.4.1",
"husky": "^7.0.4",
"msw": "^0.35.0",
"msw-storybook-addon": "^1.5.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-is": "^17.0.2",
"react-json-view": "^1.21.3",
"size-limit": "^6.0.4",
"tsdx": "^0.14.1",
"tslib": "^2.3.1",
"typescript": "^4.5.2"
},
"dependencies": {
"@medusajs/medusa": "^1.1.59",
"@medusajs/medusa-js": "^1.0.7",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"react-query": "^3.31.0"
},
"resolutions": {
"**/@typescript-eslint/eslint-plugin": "^4.11.1",
"**/@typescript-eslint/parser": "^4.11.1",
"**/jest": "^26.6.3",
"**/ts-jest": "^26.4.4",
"**/typescript": "^4.1.3"
},
"msw": {
"workerDirectory": "public"
}
}
@@ -0,0 +1,338 @@
/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker (0.35.0).
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/
const INTEGRITY_CHECKSUM = 'f0a916b13c8acc2b526a03a6d26df85f'
const bypassHeaderName = 'x-msw-bypass'
const activeClientIds = new Set()
self.addEventListener('install', function () {
return self.skipWaiting()
})
self.addEventListener('activate', async function (event) {
return self.clients.claim()
})
self.addEventListener('message', async function (event) {
const clientId = event.source.id
if (!clientId || !self.clients) {
return
}
const client = await self.clients.get(clientId)
if (!client) {
return
}
const allClients = await self.clients.matchAll()
switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
})
break
}
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: INTEGRITY_CHECKSUM,
})
break
}
case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId)
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: true,
})
break
}
case 'MOCK_DEACTIVATE': {
activeClientIds.delete(clientId)
break
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId)
const remainingClients = allClients.filter((client) => {
return client.id !== clientId
})
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister()
}
break
}
}
})
// Resolve the "master" client for the given event.
// Client that issues a request doesn't necessarily equal the client
// that registered the worker. It's with the latter the worker should
// communicate with during the response resolving phase.
async function resolveMasterClient(event) {
const client = await self.clients.get(event.clientId)
if (client.frameType === 'top-level') {
return client
}
const allClients = await self.clients.matchAll()
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible'
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id)
})
}
async function handleRequest(event, requestId) {
const client = await resolveMasterClient(event)
const response = await getResponse(event, client, requestId)
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
;(async function () {
const clonedResponse = response.clone()
sendToClient(client, {
type: 'RESPONSE',
payload: {
requestId,
type: clonedResponse.type,
ok: clonedResponse.ok,
status: clonedResponse.status,
statusText: clonedResponse.statusText,
body:
clonedResponse.body === null ? null : await clonedResponse.text(),
headers: serializeHeaders(clonedResponse.headers),
redirected: clonedResponse.redirected,
},
})
})()
}
return response
}
async function getResponse(event, client, requestId) {
const { request } = event
const requestClone = request.clone()
const getOriginalResponse = () => fetch(requestClone)
// Bypass mocking when the request client is not active.
if (!client) {
return getOriginalResponse()
}
// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return await getOriginalResponse()
}
// Bypass requests with the explicit bypass header
if (requestClone.headers.get(bypassHeaderName) === 'true') {
const cleanRequestHeaders = serializeHeaders(requestClone.headers)
// Remove the bypass header to comply with the CORS preflight check.
delete cleanRequestHeaders[bypassHeaderName]
const originalRequest = new Request(requestClone, {
headers: new Headers(cleanRequestHeaders),
})
return fetch(originalRequest)
}
// Send the request to the client-side MSW.
const reqHeaders = serializeHeaders(request.headers)
const body = await request.text()
const clientMessage = await sendToClient(client, {
type: 'REQUEST',
payload: {
id: requestId,
url: request.url,
method: request.method,
headers: reqHeaders,
cache: request.cache,
mode: request.mode,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body,
bodyUsed: request.bodyUsed,
keepalive: request.keepalive,
},
})
switch (clientMessage.type) {
case 'MOCK_SUCCESS': {
return delayPromise(
() => respondWithMock(clientMessage),
clientMessage.payload.delay,
)
}
case 'MOCK_NOT_FOUND': {
return getOriginalResponse()
}
case 'NETWORK_ERROR': {
const { name, message } = clientMessage.payload
const networkError = new Error(message)
networkError.name = name
// Rejecting a request Promise emulates a network error.
throw networkError
}
case 'INTERNAL_ERROR': {
const parsedBody = JSON.parse(clientMessage.payload.body)
console.error(
`\
[MSW] Uncaught exception in the request handler for "%s %s":
${parsedBody.location}
This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error, as it indicates a mistake in your code. If you wish to mock an error response, please see this guide: https://mswjs.io/docs/recipes/mocking-error-responses\
`,
request.method,
request.url,
)
return respondWithMock(clientMessage)
}
}
return getOriginalResponse()
}
self.addEventListener('fetch', function (event) {
const { request } = event
const accept = request.headers.get('accept') || ''
// Bypass server-sent events.
if (accept.includes('text/event-stream')) {
return
}
// Bypass navigation requests.
if (request.mode === 'navigate') {
return
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
return
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been deleted (still remains active until the next reload).
if (activeClientIds.size === 0) {
return
}
const requestId = uuidv4()
return event.respondWith(
handleRequest(event, requestId).catch((error) => {
if (error.name === 'NetworkError') {
console.warn(
'[MSW] Successfully emulated a network error for the "%s %s" request.',
request.method,
request.url,
)
return
}
// At this point, any exception indicates an issue with the original request/response.
console.error(
`\
[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`,
request.method,
request.url,
`${error.name}: ${error.message}`,
)
}),
)
})
function serializeHeaders(headers) {
const reqHeaders = {}
headers.forEach((value, name) => {
reqHeaders[name] = reqHeaders[name]
? [].concat(reqHeaders[name]).concat(value)
: value
})
return reqHeaders
}
function sendToClient(client, message) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel()
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error)
}
resolve(event.data)
}
client.postMessage(JSON.stringify(message), [channel.port2])
})
}
function delayPromise(cb, duration) {
return new Promise((resolve) => {
setTimeout(() => resolve(cb()), duration)
})
}
function respondWithMock(clientMessage) {
return new Response(clientMessage.payload.body, {
...clientMessage.payload,
headers: clientMessage.payload.headers,
})
}
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0
const v = c == 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}
@@ -0,0 +1,98 @@
import React, { useState } from "react"
import {
useAddShippingMethodToCart,
useCompleteCart,
useCreateCart,
useSetPaymentSession,
useUpdateCart,
useCreatePaymentSession,
} from "../hooks/carts"
import { Cart } from "../types"
interface CartState {
cart?: Cart
}
interface CartContext extends CartState {
setCart: (cart: Cart) => void
pay: ReturnType<typeof useSetPaymentSession>
createCart: ReturnType<typeof useCreateCart>
startCheckout: ReturnType<typeof useCreatePaymentSession>
completeCheckout: ReturnType<typeof useCompleteCart>
updateCart: ReturnType<typeof useUpdateCart>
addShippingMethod: ReturnType<typeof useAddShippingMethodToCart>
totalItems: number
}
const CartContext = React.createContext<CartContext | null>(null)
export const useCart = () => {
const context = React.useContext(CartContext)
if (!context) {
throw new Error("useCart must be used within a CartProvider")
}
return context
}
interface CartProps {
children: React.ReactNode
initialState?: Cart
}
const defaultInitialState = {
id: "",
items: [] as any,
} as Cart
export const CartProvider = ({
children,
initialState = defaultInitialState,
}: CartProps) => {
const [cart, setCart] = useState<Cart>(initialState)
const createCart = useCreateCart({
onSuccess: ({ cart }) => setCart(cart),
})
const updateCart = useUpdateCart(cart?.id, {
onSuccess: ({ cart }) => setCart(cart),
})
const addShippingMethod = useAddShippingMethodToCart(cart?.id, {
onSuccess: ({ cart }) => setCart(cart),
})
const startCheckout = useCreatePaymentSession(cart?.id, {
onSuccess: ({ cart }) => setCart(cart),
})
const pay = useSetPaymentSession(cart?.id, {
onSuccess: ({ cart }) => {
setCart(cart)
},
})
const completeCheckout = useCompleteCart(cart?.id)
const totalItems = cart?.items
.map((i) => i.quantity)
.reduce((acc, curr) => acc + curr, 0)
return (
<CartContext.Provider
value={{
cart,
setCart,
createCart,
pay,
startCheckout,
completeCheckout,
updateCart,
addShippingMethod,
totalItems: totalItems || 0,
}}
>
{children}
</CartContext.Provider>
)
}
@@ -0,0 +1,3 @@
export * from "./medusa"
export * from "./session-cart"
export * from "./cart"
@@ -0,0 +1,38 @@
import React from "react"
import { QueryClientProvider, QueryClientProviderProps } from "react-query"
import Medusa from "@medusajs/medusa-js"
interface MedusaContextState {
client: Medusa
}
const MedusaContext = React.createContext<MedusaContextState | null>(null)
export const useMedusa = () => {
const context = React.useContext(MedusaContext)
if (!context) {
throw new Error("useMedusa must be used within a MedusaProvider")
}
return context
}
interface MedusaProviderProps {
baseUrl: string
queryClientProviderProps: QueryClientProviderProps
children: React.ReactNode
}
export const MedusaProvider = ({
queryClientProviderProps,
baseUrl,
children,
}: MedusaProviderProps) => {
const medusaClient = new Medusa({ baseUrl, maxRetries: 0 })
return (
<QueryClientProvider {...queryClientProviderProps}>
<MedusaContext.Provider value={{ client: medusaClient }}>
{children}
</MedusaContext.Provider>
</QueryClientProvider>
)
}
@@ -0,0 +1,289 @@
import React, { useContext, useEffect } from "react"
import { useLocalStorage } from "../hooks/utils"
import { RegionInfo, ProductVariant } from "../types"
import { getVariantPrice } from "../utils"
import { isArray, isEmpty, isObject } from "lodash"
interface Item {
variant: ProductVariant
quantity: number
readonly total?: number
}
export interface SessionCartState {
region: RegionInfo
items: Item[]
totalItems: number
total: number
}
interface SessionCartContextState extends SessionCartState {
setRegion: (region: RegionInfo) => void
addItem: (item: Item) => void
removeItem: (id: string) => void
updateItem: (id: string, item: Partial<Item>) => void
setItems: (items: Item[]) => void
updateItemQuantity: (id: string, quantity: number) => void
incrementItemQuantity: (id: string) => void
decrementItemQuantity: (id: string) => void
getItem: (id: string) => Item | undefined
clearItems: () => void
}
const SessionCartContext = React.createContext<SessionCartContextState | null>(
null
)
enum ACTION_TYPES {
INIT,
ADD_ITEM,
SET_ITEMS,
REMOVE_ITEM,
UPDATE_ITEM,
CLEAR_ITEMS,
SET_REGION,
}
type Action =
| { type: ACTION_TYPES.SET_REGION; payload: RegionInfo }
| { type: ACTION_TYPES.INIT; payload: object }
| { type: ACTION_TYPES.ADD_ITEM; payload: Item }
| {
type: ACTION_TYPES.UPDATE_ITEM
payload: { id: string; item: Partial<Item> }
}
| { type: ACTION_TYPES.REMOVE_ITEM; payload: { id: string } }
| { type: ACTION_TYPES.SET_ITEMS; payload: Item[] }
| { type: ACTION_TYPES.CLEAR_ITEMS }
const reducer = (state: SessionCartState, action: Action) => {
switch (action.type) {
case ACTION_TYPES.INIT: {
return state
}
case ACTION_TYPES.SET_REGION: {
return generateCartState(
{
...state,
region: action.payload,
},
state.items
)
}
case ACTION_TYPES.ADD_ITEM: {
const duplicateVariantIndex = state.items.findIndex(
(item) => item.variant.id === action.payload?.variant?.id
)
if (duplicateVariantIndex !== -1) {
state.items.splice(duplicateVariantIndex, 1)
}
const items = [...state.items, action.payload]
return generateCartState(state, items)
}
case ACTION_TYPES.UPDATE_ITEM: {
const items = state.items.map((item) =>
item.variant.id === action.payload.id
? { ...item, ...action.payload.item }
: item
)
return generateCartState(state, items)
}
case ACTION_TYPES.REMOVE_ITEM: {
const items = state.items.filter(
(item) => item.variant.id !== action.payload.id
)
return generateCartState(state, items)
}
case ACTION_TYPES.SET_ITEMS: {
return generateCartState(state, action.payload)
}
case ACTION_TYPES.CLEAR_ITEMS: {
return {
...state,
items: [],
total: 0,
totalItems: 0,
}
}
default:
return state
}
}
export const generateCartState = (state: SessionCartState, items: Item[]) => {
const newItems = generateItems(state.region, items)
return {
...state,
items: newItems,
totalItems: items.reduce((sum, item) => sum + item.quantity, 0),
total: calculateSessionCartTotal(newItems),
}
}
const generateItems = (region: RegionInfo, items: Item[]) => {
return items.map((item) => ({
...item,
total: getVariantPrice(item.variant, region),
}))
}
const calculateSessionCartTotal = (items: Item[]) => {
return items.reduce(
(total, item) => total + item.quantity * (item.total || 0),
0
)
}
interface SessionCartProviderProps {
children: React.ReactNode
initialState?: SessionCartState
}
const defaultInitialState: SessionCartState = {
region: {} as RegionInfo,
items: [],
total: 0,
totalItems: 0,
}
export const SessionCartProvider = ({
initialState = defaultInitialState,
children,
}: SessionCartProviderProps) => {
const [saved, save] = useLocalStorage(
"medusa-session-cart",
JSON.stringify(initialState)
)
const [state, dispatch] = React.useReducer(reducer, JSON.parse(saved))
useEffect(() => {
save(JSON.stringify(state))
}, [state, save])
const setRegion = (region: RegionInfo) => {
if (!isObject(region) || isEmpty(region)) {
throw new Error("region must be a non-empty object")
}
dispatch({ type: ACTION_TYPES.SET_REGION, payload: region })
}
const getItem = (id: string) => {
return state.items.find((item) => item.variant.id === id)
}
const setItems = (items: Item[]) => {
if (!isArray(items)) {
throw new Error("items must be an array of items")
}
dispatch({ type: ACTION_TYPES.SET_ITEMS, payload: items })
}
const addItem = (item: Item) => {
if (!isObject(item) || isEmpty(item)) {
throw new Error("item must be a non-empty object")
}
dispatch({ type: ACTION_TYPES.ADD_ITEM, payload: item })
}
const updateItem = (id: string, item: Partial<Item>) => {
dispatch({ type: ACTION_TYPES.UPDATE_ITEM, payload: { id, item } })
}
const updateItemQuantity = (id: string, quantity: number) => {
const item = getItem(id)
if (!item) return
quantity = quantity <= 0 ? 1 : quantity
dispatch({
type: ACTION_TYPES.UPDATE_ITEM,
payload: {
id,
item: {
...item,
quantity: Math.min(item.variant.inventory_quantity, quantity),
},
},
})
}
const incrementItemQuantity = (id: string) => {
const item = getItem(id)
if (!item) return
dispatch({
type: ACTION_TYPES.UPDATE_ITEM,
payload: {
id,
item: {
...item,
quantity: Math.min(
item.variant.inventory_quantity,
item.quantity + 1
),
},
},
})
}
const decrementItemQuantity = (id: string) => {
const item = getItem(id)
if (!item) return
dispatch({
type: ACTION_TYPES.UPDATE_ITEM,
payload: {
id,
item: { ...item, quantity: Math.max(0, item.quantity - 1) },
},
})
}
const removeItem = (id: string) => {
dispatch({
type: ACTION_TYPES.REMOVE_ITEM,
payload: { id },
})
}
const clearItems = () => {
dispatch({
type: ACTION_TYPES.CLEAR_ITEMS,
})
}
return (
<SessionCartContext.Provider
value={{
...state,
setRegion,
addItem,
updateItem,
updateItemQuantity,
incrementItemQuantity,
decrementItemQuantity,
removeItem,
getItem,
setItems,
clearItems,
}}
>
{children}
</SessionCartContext.Provider>
)
}
export const useSessionCart = () => {
const context = useContext(SessionCartContext)
if (!context) {
throw new Error(
"useSessionCart should be used as a child of SessionCartProvider"
)
}
return context
}
@@ -0,0 +1,2 @@
export * from "./queries"
export * from "./mutations"
@@ -0,0 +1,156 @@
import {
StoreCartsRes,
StoreCompleteCartRes,
StorePostCartReq,
StorePostCartsCartPaymentSessionReq,
StorePostCartsCartPaymentSessionUpdateReq,
StorePostCartsCartReq,
StorePostCartsCartShippingMethodReq,
} from "@medusajs/medusa"
import { useMutation, UseMutationOptions } from "react-query"
import { useMedusa } from "../../contexts/medusa"
export const useCreateCart = (
options?: UseMutationOptions<
StoreCartsRes,
Error,
StorePostCartReq | undefined
>
) => {
const { client } = useMedusa()
return useMutation(
(data?: StorePostCartReq | undefined) => client.carts.create(data),
options
)
}
export const useUpdateCart = (
cartId: string,
options?: UseMutationOptions<StoreCartsRes, Error, StorePostCartsCartReq>
) => {
const { client } = useMedusa()
return useMutation(
(data: StorePostCartsCartReq) => client.carts.update(cartId, data),
options
)
}
export const useCompleteCart = (
cartId: string,
options?: UseMutationOptions<StoreCompleteCartRes, Error>
) => {
const { client } = useMedusa()
return useMutation(() => client.carts.complete(cartId), options)
}
export const useCreatePaymentSession = (
cartId: string,
options?: UseMutationOptions<StoreCartsRes, Error>
) => {
const { client } = useMedusa()
return useMutation(() => client.carts.createPaymentSessions(cartId), options)
}
export const useUpdatePaymentSession = (
cartId: string,
options?: UseMutationOptions<
StoreCartsRes,
Error,
{ provider_id: string } & StorePostCartsCartPaymentSessionUpdateReq
>
) => {
const { client } = useMedusa()
return useMutation(
(
data: { provider_id: string } & StorePostCartsCartPaymentSessionUpdateReq
) => client.carts.updatePaymentSession(cartId, data.provider_id, data),
options
)
}
type RefreshPaymentSessionMutationData = {
provider_id: string
}
export const useRefreshPaymentSession = (
cartId: string,
options?: UseMutationOptions<
StoreCartsRes,
Error,
RefreshPaymentSessionMutationData
>
) => {
const { client } = useMedusa()
return useMutation(
({ provider_id }: RefreshPaymentSessionMutationData) =>
client.carts.refreshPaymentSession(cartId, provider_id),
options
)
}
type SetPaymentSessionMutationData = { provider_id: string }
export const useSetPaymentSession = (
cartId: string,
options?: UseMutationOptions<
StoreCartsRes,
Error,
SetPaymentSessionMutationData
>
) => {
const { client } = useMedusa()
return useMutation(
(data: StorePostCartsCartPaymentSessionReq) =>
client.carts.setPaymentSession(cartId, data),
options
)
}
export const useAddShippingMethodToCart = (
cartId: string,
options?: UseMutationOptions<
StoreCartsRes,
Error,
StorePostCartsCartShippingMethodReq
>
) => {
const { client } = useMedusa()
return useMutation(
(data: StorePostCartsCartShippingMethodReq) =>
client.carts.addShippingMethod(cartId, data),
options
)
}
type DeletePaymentSessionMutationData = {
provider_id: string
}
export const useDeletePaymentSession = (
cartId: string,
options?: UseMutationOptions<
StoreCartsRes,
Error,
DeletePaymentSessionMutationData
>
) => {
const { client } = useMedusa()
return useMutation(
({ provider_id }: DeletePaymentSessionMutationData) =>
client.carts.deletePaymentSession(cartId, provider_id),
options
)
}
export const useStartCheckout = (
options?: UseMutationOptions<StoreCartsRes["cart"], Error, StorePostCartReq>
) => {
const { client } = useMedusa()
const mutation = useMutation(async (data?: StorePostCartReq) => {
const { cart } = await client.carts.create(data)
const res = await client.carts.createPaymentSessions(cart.id)
return res.cart
}, options)
return mutation
}
@@ -0,0 +1,28 @@
import { makeKeysFactory } from "./../utils/index"
import { StoreCartsRes } from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useQuery } from "react-query"
import { useMedusa } from "../../contexts/medusa"
import { UseQueryOptionsWrapper } from "../../types"
const CARTS_QUERY_KEY = `carts` as const
export const cartKeys = makeKeysFactory(CARTS_QUERY_KEY)
type CartQueryKey = typeof cartKeys
export const useGetCart = (
id: string,
options?: UseQueryOptionsWrapper<
Response<StoreCartsRes>,
Error,
ReturnType<CartQueryKey["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
cartKeys.detail(id),
() => client.carts.retrieve(id),
options
)
return { ...data, ...rest } as const
}
@@ -0,0 +1 @@
export * from "./queries"
@@ -0,0 +1,50 @@
import {
StoreCollectionsListRes,
StoreCollectionsRes,
StoreGetCollectionsParams,
} from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useQuery } from "react-query"
import { useMedusa } from "../../contexts/medusa"
import { UseQueryOptionsWrapper } from "./../../types"
import { makeKeysFactory } from "./../utils/index"
const COLLECTIONS_QUERY_KEY = `collections` as const
export const collectionKeys = makeKeysFactory(COLLECTIONS_QUERY_KEY)
type CollectionQueryKey = typeof collectionKeys
export const useCollection = (
id: string,
options?: UseQueryOptionsWrapper<
Response<StoreCollectionsRes>,
Error,
ReturnType<CollectionQueryKey["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
collectionKeys.detail(id),
() => client.collections.retrieve(id),
options
)
return { ...data, ...rest } as const
}
export const useCollections = (
query?: StoreGetCollectionsParams,
options?: UseQueryOptionsWrapper<
Response<StoreCollectionsListRes>,
Error,
ReturnType<CollectionQueryKey["list"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
collectionKeys.list(query),
() => client.collections.list(query),
options
)
return { ...data, ...rest } as const
}
@@ -0,0 +1,2 @@
export * from "./queries"
export * from "./mutations"
@@ -0,0 +1,32 @@
import {
StoreCustomersRes,
StorePostCustomersCustomerReq,
StorePostCustomersReq,
} from "@medusajs/medusa"
import { useMutation, UseMutationOptions } from "react-query"
import { useMedusa } from "../../contexts/medusa"
export const useCreateCustomer = (
options?: UseMutationOptions<StoreCustomersRes, Error, StorePostCustomersReq>
) => {
const { client } = useMedusa()
return useMutation(
(data: StorePostCustomersReq) => client.customers.create(data),
options
)
}
export const useUpdateMe = (
options?: UseMutationOptions<
StoreCustomersRes,
Error,
{ id: string } & StorePostCustomersCustomerReq
>
) => {
const { client } = useMedusa()
return useMutation(
({ id, ...data }: { id: string } & StorePostCustomersCustomerReq) =>
client.customers.update(data),
options
)
}
@@ -0,0 +1,53 @@
import {
StoreCustomersListOrdersRes,
StoreCustomersRes,
StoreGetCustomersCustomerOrdersParams,
} from "@medusajs/medusa"
import { useQuery } from "react-query"
import { Response } from "@medusajs/medusa-js"
import { useMedusa } from "../../contexts"
import { UseQueryOptionsWrapper } from "../../types"
import { makeKeysFactory } from "./../utils/index"
const CUSTOMERS_QUERY_KEY = `customers` as const
export const customerKeys = {
...makeKeysFactory(CUSTOMERS_QUERY_KEY),
orders: (id: string) => [...customerKeys.detail(id), "orders"] as const,
}
type CustomerQueryKey = typeof customerKeys
export const useMeCustomer = (
options?: UseQueryOptionsWrapper<
Response<StoreCustomersRes>,
Error,
ReturnType<CustomerQueryKey["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
customerKeys.detail("me"),
() => client.customers.retrieve(),
options
)
return { ...data, ...rest } as const
}
export const useCustomerOrders = (
query: StoreGetCustomersCustomerOrdersParams = { limit: 10, offset: 0 },
options?: UseQueryOptionsWrapper<
Response<StoreCustomersListOrdersRes>,
Error,
ReturnType<CustomerQueryKey["orders"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
customerKeys.orders("me"),
() => client.customers.listOrders(query),
options
)
return { ...data, ...rest } as const
}
@@ -0,0 +1 @@
export * from "./queries"
@@ -0,0 +1,29 @@
import { makeKeysFactory } from "./../utils/index"
import { StoreGiftCardsRes } from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useQuery } from "react-query"
import { useMedusa } from "../../contexts"
import { UseQueryOptionsWrapper } from "../../types"
const GIFT_CARDS_QUERY_KEY = `gift_cards` as const
export const giftCardKeys = makeKeysFactory(GIFT_CARDS_QUERY_KEY)
type GiftCardQueryKey = typeof giftCardKeys
export const useGiftCard = (
id: string,
options?: UseQueryOptionsWrapper<
Response<StoreGiftCardsRes>,
Error,
ReturnType<GiftCardQueryKey["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
giftCardKeys.detail(id),
() => client.giftCards.retrieve(id),
options
)
return { ...data, ...rest } as const
}
+13
View File
@@ -0,0 +1,13 @@
export * from "./products/"
export * from "./carts/"
export * from "./shipping-options/"
export * from "./regions/"
export * from "./return-reasons/"
export * from "./swaps/"
export * from "./carts/"
export * from "./orders/"
export * from "./customers/"
export * from "./returns/"
export * from "./gift-cards/"
export * from "./line-items/"
export * from "./collections"
@@ -0,0 +1 @@
export * from "./mutations"
@@ -0,0 +1,54 @@
import {
StoreCartsRes,
StorePostCartsCartLineItemsReq,
StorePostCartsCartLineItemsItemReq,
} from "@medusajs/medusa"
import { useMutation, UseMutationOptions } from "react-query"
import { useMedusa } from "../../contexts"
export const useCreateLineItem = (
cartId: string,
options?: UseMutationOptions<
StoreCartsRes,
Error,
StorePostCartsCartLineItemsReq
>
) => {
const { client } = useMedusa()
return useMutation(
(data: StorePostCartsCartLineItemsReq) =>
client.carts.lineItems.create(cartId, data),
options
)
}
export const useUpdateLineItem = (
cartId: string,
options?: UseMutationOptions<
StoreCartsRes,
Error,
StorePostCartsCartLineItemsItemReq & { lineId: string }
>
) => {
const { client } = useMedusa()
return useMutation(
({
lineId,
...data
}: StorePostCartsCartLineItemsItemReq & { lineId: string }) =>
client.carts.lineItems.update(cartId, lineId, data),
options
)
}
export const useDeleteLineItem = (
cartId: string,
options?: UseMutationOptions<StoreCartsRes, Error, { lineId: string }>
) => {
const { client } = useMedusa()
return useMutation(
({ lineId }: { lineId: string }) =>
client.carts.lineItems.delete(cartId, lineId),
options
)
}
@@ -0,0 +1 @@
export * from "./queries"
@@ -0,0 +1,71 @@
import { makeKeysFactory } from "./../utils/index"
import { StoreOrdersRes, StoreGetOrdersParams } from "@medusajs/medusa"
import { useQuery } from "react-query"
import { useMedusa } from "../../contexts"
import { UseQueryOptionsWrapper } from "../../types"
import { Response } from "@medusajs/medusa-js"
const ORDERS_QUERY_KEY = `orders` as const
export const orderKeys = {
...makeKeysFactory<typeof ORDERS_QUERY_KEY, StoreGetOrdersParams>(
ORDERS_QUERY_KEY
),
cart: (cartId: string) => [...orderKeys.details(), "cart", cartId] as const,
}
type OrderQueryKey = typeof orderKeys
export const useOrder = (
id: string,
options?: UseQueryOptionsWrapper<
Response<StoreOrdersRes>,
Error,
ReturnType<OrderQueryKey["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
orderKeys.detail(id),
() => client.orders.retrieve(id),
options
)
return { ...data, ...rest } as const
}
export const useCartOrder = (
cartId: string,
options?: UseQueryOptionsWrapper<
Response<StoreOrdersRes>,
Error,
ReturnType<OrderQueryKey["cart"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
orderKeys.cart(cartId),
() => client.orders.retrieveByCartId(cartId),
options
)
return { ...data, ...rest } as const
}
export const useOrderLookup = (
query: StoreGetOrdersParams,
options?: UseQueryOptionsWrapper<
Response<StoreOrdersRes>,
Error,
ReturnType<OrderQueryKey["list"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
orderKeys.list(query),
() => client.orders.lookupOrder(query),
options
)
return { ...data, ...rest } as const
}
@@ -0,0 +1 @@
export * from "./queries"
@@ -0,0 +1,53 @@
import { Response } from "@medusajs/medusa-js"
import {
StoreGetProductsParams,
StoreProductsListRes,
StoreProductsRes,
} from "@medusajs/medusa"
import { useQuery } from "react-query"
import { useMedusa } from "../../contexts"
import { UseQueryOptionsWrapper } from "../../types"
import { makeKeysFactory } from "./../utils/index"
const PRODUCTS_QUERY_KEY = `products` as const
export const productKeys = makeKeysFactory<
typeof PRODUCTS_QUERY_KEY,
StoreGetProductsParams
>(PRODUCTS_QUERY_KEY)
type ProductQueryKey = typeof productKeys
export const useProducts = (
query?: StoreGetProductsParams,
options?: UseQueryOptionsWrapper<
Response<StoreProductsListRes>,
Error,
ReturnType<ProductQueryKey["list"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
productKeys.list(query),
() => client.products.list(query),
options
)
return { ...data, ...rest } as const
}
export const useProduct = (
id: string,
options?: UseQueryOptionsWrapper<
Response<StoreProductsRes>,
Error,
ReturnType<ProductQueryKey["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
productKeys.detail(id),
() => client.products.retrieve(id),
options
)
return { ...data, ...rest } as const
}
@@ -0,0 +1 @@
export * from "./queries"
@@ -0,0 +1,45 @@
import { makeKeysFactory } from "./../utils/index"
import { UseQueryOptionsWrapper } from "../../types"
import { useQuery } from "react-query"
import { useMedusa } from "../../contexts"
import { StoreRegionsRes, StoreRegionsListRes } from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
const REGIONS_QUERY_KEY = `regions` as const
const regionsKey = makeKeysFactory(REGIONS_QUERY_KEY)
type RegionQueryType = typeof regionsKey
export const useRegions = (
options?: UseQueryOptionsWrapper<
Response<StoreRegionsListRes>,
Error,
ReturnType<RegionQueryType["lists"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
regionsKey.lists(),
() => client.regions.list(),
options
)
return { ...data, ...rest } as const
}
export const useRegion = (
id: string,
options?: UseQueryOptionsWrapper<
Response<StoreRegionsRes>,
Error,
ReturnType<RegionQueryType["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
regionsKey.detail(id),
() => client.regions.retrieve(id),
options
)
return { ...data, ...rest } as const
}
@@ -0,0 +1 @@
export * from "./queries"
@@ -0,0 +1,48 @@
import { makeKeysFactory } from "./../utils/index"
import {
StoreReturnReasonsListRes,
StoreReturnReasonsRes,
} from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useQuery } from "react-query"
import { useMedusa } from "../../contexts"
import { UseQueryOptionsWrapper } from "../../types"
const RETURNS_REASONS_QUERY_KEY = `return_reasons` as const
const returnReasonsKey = makeKeysFactory(RETURNS_REASONS_QUERY_KEY)
type ReturnReasonsQueryKey = typeof returnReasonsKey
export const useReturnReasons = (
options?: UseQueryOptionsWrapper<
Response<StoreReturnReasonsListRes>,
Error,
ReturnType<ReturnReasonsQueryKey["lists"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
returnReasonsKey.lists(),
() => client.returnReasons.list(),
options
)
return { ...data, ...rest } as const
}
export const useReturnReason = (
id: string,
options?: UseQueryOptionsWrapper<
Response<StoreReturnReasonsRes>,
Error,
ReturnType<ReturnReasonsQueryKey["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
returnReasonsKey.detail(id),
() => client.returnReasons.retrieve(id),
options
)
return { ...data, ...rest } as const
}
@@ -0,0 +1 @@
export * from "./mutations"
@@ -0,0 +1,13 @@
import { StoreReturnsRes, StorePostReturnsReq } from "@medusajs/medusa"
import { useMutation, UseMutationOptions } from "react-query"
import { useMedusa } from "../../contexts"
export const useCreateReturn = (
options?: UseMutationOptions<StoreReturnsRes, Error, StorePostReturnsReq>
) => {
const { client } = useMedusa()
return useMutation(
(data: StorePostReturnsReq) => client.returns.create(data),
options
)
}
@@ -0,0 +1 @@
export * from "./queries"
@@ -0,0 +1,52 @@
import { makeKeysFactory } from "./../utils/index"
import { UseQueryOptionsWrapper } from "../../types"
import { useQuery } from "react-query"
import { useMedusa } from "../../contexts"
import {
StoreShippingOptionsListRes,
StoreGetShippingOptionsParams,
} from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
const SHIPPING_OPTION_QUERY_KEY = `shipping_options` as const
const shippingOptionKey = {
...makeKeysFactory(SHIPPING_OPTION_QUERY_KEY),
cart: (cartId: string) => [...shippingOptionKey.all, "cart", cartId] as const,
}
type ShippingOptionQueryKey = typeof shippingOptionKey
export const useShippingOptions = (
query?: StoreGetShippingOptionsParams,
options?: UseQueryOptionsWrapper<
Response<StoreShippingOptionsListRes>,
Error,
ReturnType<ShippingOptionQueryKey["list"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
shippingOptionKey.list(query),
async () => client.shippingOptions.list(query),
options
)
return { ...data, ...rest } as const
}
export const useCartShippingOptions = (
cartId: string,
options?: UseQueryOptionsWrapper<
Response<StoreShippingOptionsListRes>,
Error,
ReturnType<ShippingOptionQueryKey["cart"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
shippingOptionKey.cart(cartId),
async () => client.shippingOptions.listCartOptions(cartId),
options
)
return { ...data, ...rest } as const
}
@@ -0,0 +1,2 @@
export * from "./queries"
export * from "./mutations"
@@ -0,0 +1,13 @@
import { StoreSwapsRes, StorePostSwapsReq } from "@medusajs/medusa"
import { useMutation, UseMutationOptions } from "react-query"
import { useMedusa } from "../../contexts"
export const useCreateSwap = (
options?: UseMutationOptions<StoreSwapsRes, Error, StorePostSwapsReq>
) => {
const { client } = useMedusa()
return useMutation(
(data: StorePostSwapsReq) => client.swaps.create(data),
options
)
}
@@ -0,0 +1,33 @@
import { makeKeysFactory } from "./../utils/index"
import { StoreSwapsRes } from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useQuery } from "react-query"
import { useMedusa } from "../../contexts"
import { UseQueryOptionsWrapper } from "../../types"
const SWAPS_QUERY_KEY = `swaps` as const
const swapKey = {
...makeKeysFactory(SWAPS_QUERY_KEY),
cart: (cartId: string) => [...swapKey.all, "cart", cartId] as const,
}
type SwapQueryKey = typeof swapKey
export const useCartSwap = (
cartId: string,
options?: UseQueryOptionsWrapper<
Response<StoreSwapsRes>,
Error,
ReturnType<SwapQueryKey["cart"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
swapKey.cart(cartId),
() => client.swaps.retrieveByCartId(cartId),
options
)
return { ...data, ...rest } as const
}
@@ -0,0 +1,62 @@
import * as React from "react"
type TQueryKey<TKey, TListQuery = any, TDetailQuery = string> = {
all: [TKey]
lists: () => [...TQueryKey<TKey>["all"], "list"]
list: (
query?: TListQuery
) => [
...ReturnType<TQueryKey<TKey>["lists"]>,
{ query: TListQuery | undefined }
]
details: () => [...TQueryKey<TKey>["all"], "detail"]
detail: (
id: TDetailQuery
) => [...ReturnType<TQueryKey<TKey>["details"]>, TDetailQuery]
}
export const makeKeysFactory = <
T,
TListQueryType = any,
TDetailQueryType = string
>(
globalKey: T
) => {
const queryKeyFactory: TQueryKey<T, TListQueryType, TDetailQueryType> = {
all: [globalKey],
lists: () => [...queryKeyFactory.all, "list"],
list: (query?: TListQueryType) => [...queryKeyFactory.lists(), { query }],
details: () => [...queryKeyFactory.all, "detail"],
detail: (id: TDetailQueryType) => [...queryKeyFactory.details(), id],
}
return queryKeyFactory
}
export const useLocalStorage = (key: string, initialState: string) => {
const [item, setItem] = React.useState(() => {
try {
const item =
typeof window !== "undefined" && window.localStorage.getItem(key)
return item || initialState
} catch (err) {
return initialState
}
})
const save = (data: string) => {
setItem(data)
if (typeof window !== "undefined") {
window.localStorage.setItem(key, data)
}
}
const remove = () => {
if (typeof window !== "undefined") {
window.localStorage.removeItem(key)
}
}
return [item, save, remove] as const
}
+3
View File
@@ -0,0 +1,3 @@
export * from "./contexts"
export * from "./hooks/"
export * from "./utils/"
+31
View File
@@ -0,0 +1,31 @@
import {
Region,
ProductVariant as ProductVariantEntity,
StoreCartsRes,
} from "@medusajs/medusa"
import { QueryKey, UseQueryOptions } from "react-query"
export type UseQueryOptionsWrapper<
// Return type of queryFn
TQueryFn = unknown,
// Type thrown in case the queryFn rejects
E = Error,
// Query key type
TQueryKey extends QueryKey = QueryKey
> = Omit<
UseQueryOptions<TQueryFn, E, TQueryFn, TQueryKey>,
"queryKey" | "queryFn" | "select" | "refetchInterval"
>
// Choose only a subset of the type Region to allow for some flexibility
export type RegionInfo = Pick<Region, "currency_code" | "tax_code" | "tax_rate">
export type ProductVariant = ConvertDateToString<
Omit<ProductVariantEntity, "beforeInsert">
>
export type ProductVariantInfo = Pick<ProductVariant, "prices">
type ConvertDateToString<T extends {}> = {
[P in keyof T]: T[P] extends Date ? Date | string : T[P]
}
export type Cart = StoreCartsRes["cart"]
+168
View File
@@ -0,0 +1,168 @@
import { isEmpty } from "lodash"
import { RegionInfo, ProductVariantInfo } from "../types"
type FormatVariantPriceParams = {
variant: ProductVariantInfo
region: RegionInfo
includeTaxes?: boolean
minimumFractionDigits?: number
maximumFractionDigits?: number
locale?: string
}
/**
* Takes a product variant and a region, and converts the variant's price to a localized decimal format
*/
export const formatVariantPrice = ({
variant,
region,
includeTaxes = true,
...rest
}: FormatVariantPriceParams) => {
const amount = computeVariantPrice({ variant, region, includeTaxes })
return convertToLocale({
amount,
currency_code: region?.currency_code,
...rest,
})
}
type ComputeVariantPriceParams = {
variant: ProductVariantInfo
region: RegionInfo
includeTaxes?: boolean
}
/**
* Takes a product variant and region, and returns the variant price as a decimal number
* @param params.variant - product variant
* @param params.region - region
* @param params.includeTaxes - whether to include taxes or not
*/
export const computeVariantPrice = ({
variant,
region,
includeTaxes = true,
}: ComputeVariantPriceParams) => {
const amount = getVariantPrice(variant, region)
return computeAmount({
amount,
region,
includeTaxes,
})
}
/**
* Finds the price amount correspoding to the region selected
* @param variant - the product variant
* @param region - the region
* @returns - the price's amount
*/
export const getVariantPrice = (
variant: ProductVariantInfo,
region: RegionInfo
) => {
let price = variant?.prices?.find(
(p) =>
p.currency_code.toLowerCase() === region?.currency_code?.toLowerCase()
)
return price?.amount || 0
}
type ComputeAmountParams = {
amount: number
region: RegionInfo
includeTaxes?: boolean
}
/**
* Takes an amount, a region, and returns the amount as a decimal including or excluding taxes
*/
export const computeAmount = ({
amount,
region,
includeTaxes = true,
}: ComputeAmountParams) => {
const toDecimal = convertToDecimal(amount, region)
const taxRate = includeTaxes ? getTaxRate(region) : 0
const amountWithTaxes = toDecimal * (1 + taxRate)
return amountWithTaxes
}
type FormatAmountParams = {
amount: number
region: RegionInfo
includeTaxes?: boolean
minimumFractionDigits?: number
maximumFractionDigits?: number
locale?: string
}
/**
* Takes an amount and a region, and converts the amount to a localized decimal format
*/
export const formatAmount = ({
amount,
region,
includeTaxes = true,
...rest
}: FormatAmountParams) => {
const taxAwareAmount = computeAmount({
amount,
region,
includeTaxes,
})
return convertToLocale({
amount: taxAwareAmount,
currency_code: region.currency_code,
...rest,
})
}
// we should probably add a more extensive list
const noDivisionCurrencies = ["krw", "jpy", "vnd"]
const convertToDecimal = (amount: number, region: RegionInfo) => {
const divisor = noDivisionCurrencies.includes(
region?.currency_code?.toLowerCase()
)
? 1
: 100
return Math.floor(amount) / divisor
}
const getTaxRate = (region?: RegionInfo) => {
return region && !isEmpty(region) ? region?.tax_rate / 100 : 0
}
const convertToLocale = ({
amount,
currency_code,
minimumFractionDigits,
maximumFractionDigits,
locale = "en-US",
}: ConvertToLocaleParams) => {
return currency_code && !isEmpty(currency_code)
? new Intl.NumberFormat(locale, {
style: "currency",
currency: currency_code,
minimumFractionDigits,
maximumFractionDigits,
}).format(amount)
: amount.toString()
}
type ConvertToLocaleParams = {
amount: number
currency_code: string
minimumFractionDigits?: number
maximumFractionDigits?: number
locale?: string
}
@@ -0,0 +1,50 @@
import { Meta } from "@storybook/react"
import React from "react"
import { useGetCart } from "../src"
import Layout from "./components/Layout"
const Cart = ({ showHookData, id }) => {
const data = useGetCart(id)
return (
<Layout showHookData={showHookData} data={data}>
<h3>Cart: {id}</h3>
<div>
{data?.cart?.email} - {data?.cart?.total}
</div>
</Layout>
)
}
const meta: Meta = {
title: "Carts",
argTypes: {
showHookData: {
name: "Show hook data",
description:
"Whether or not story should display JSON of data returned from hook",
control: {
type: "boolean",
},
defaultValue: true,
},
},
parameters: {
controls: { expanded: true },
},
}
export default meta
export const GetOne = (args: { showHookData: boolean; id: string }) => (
<Cart {...args} />
)
GetOne.argTypes = {
id: {
control: {
type: "text",
},
name: "cart id",
defaultValue: "cart_...",
},
}
@@ -0,0 +1,86 @@
import { Meta } from "@storybook/react"
import React from "react"
import { useCollection, useCollections } from "../src"
import Layout from "./components/Layout"
const Collections = ({ showHookData, limit, offset }) => {
const data = useCollections({ limit, offset })
return (
<Layout showHookData={showHookData} data={data}>
<h3>Collections</h3>
<ul>
{data?.collections?.map((Collection) => (
<li key={Collection.id}>{Collection.title}</li>
))}
</ul>
</Layout>
)
}
const Collection = ({ showHookData, id }) => {
const data = useCollection(id)
return (
<Layout showHookData={showHookData} data={data}>
<h3>Collection: {id}</h3>
<div>
{data?.collection?.title} - {data?.collection?.handle}
</div>
</Layout>
)
}
const meta: Meta = {
title: "Collections",
argTypes: {
showHookData: {
name: "Show hook data",
description:
"Whether or not story should display JSON of data returned from hook",
control: {
type: "boolean",
},
defaultValue: true,
},
},
parameters: {
controls: { expanded: true },
},
}
export default meta
export const List = (args: {
limit: number
offset: number
showHookData: boolean
}) => <Collections {...args} />
List.argTypes = {
limit: {
control: {
type: "number",
},
name: "limit",
defaultValue: 5,
},
offset: {
control: {
type: "number",
},
defaultValue: 0,
},
}
export const GetOne = (args: { showHookData: boolean; id: string }) => (
<Collection {...args} />
)
GetOne.argTypes = {
id: {
control: {
type: "text",
},
name: "Collection id",
defaultValue: "pcol_",
},
}
@@ -0,0 +1,69 @@
import { Meta } from "@storybook/react"
import React from "react"
import { useMeCustomer, useCustomerOrders } from "../src"
import Layout from "./components/Layout"
const CustomerOrders = ({ showHookData, id }) => {
const data = useCustomerOrders(id)
return (
<Layout showHookData={showHookData} data={data}>
<h3>Customer Orders</h3>
<ul>
{data?.orders?.map((order) => (
<li key={order.id}>price: {order.total}</li>
))}
</ul>
</Layout>
)
}
const Customer = ({ showHookData }) => {
const data = useMeCustomer()
return (
<Layout showHookData={showHookData} data={data}>
<h3>Customer: </h3>
<div>
{data?.customer?.first_name} {data?.customer?.last_name} -{" "}
{data?.customer?.email}
</div>
</Layout>
)
}
const meta: Meta = {
title: "Customers",
argTypes: {
showHookData: {
name: "Show hook data",
description:
"Whether or not story should display JSON of data returned from hook",
control: {
type: "boolean",
},
defaultValue: true,
},
},
parameters: {
controls: { expanded: true },
},
}
export default meta
export const ListOrders = (args: { showHookData: boolean; id: string }) => (
<CustomerOrders {...args} />
)
ListOrders.argTypes = {
id: {
control: {
type: "text",
},
name: "customer id",
defaultValue: "reg_",
},
}
export const GetOne = (args: { showHookData: boolean }) => (
<Customer {...args} />
)
@@ -0,0 +1,76 @@
import { Meta } from "@storybook/react"
import React from "react"
import { useCartOrder, useOrder, useProduct, useProducts } from "../src"
import Layout from "./components/Layout"
const CartOrderComponent = ({ showHookData, cartId }) => {
const data = useCartOrder(cartId)
return (
<Layout showHookData={showHookData} data={data}>
<h3>Order from cart with id: {cartId}</h3>
<div>
{data?.order?.email} - {data?.order?.currency_code}
</div>
</Layout>
)
}
const Order = ({ showHookData, id }) => {
const data = useOrder(id)
return (
<Layout showHookData={showHookData} data={data}>
<h3>Order: {id}</h3>
<div>
{data?.order?.email} - {data?.order?.currency_code}
</div>
</Layout>
)
}
const meta: Meta = {
title: "Orders",
argTypes: {
showHookData: {
name: "Show hook data",
description:
"Whether or not story should display JSON of data returned from hook",
control: {
type: "boolean",
},
defaultValue: true,
},
},
parameters: {
controls: { expanded: true },
},
}
export default meta
export const CartOrder = (args: { cartId: string; showHookData: boolean }) => (
<CartOrderComponent {...args} />
)
CartOrder.argTypes = {
cartId: {
control: {
type: "text",
},
name: "cart id",
defaultValue: "cart_...",
},
}
export const GetOne = (args: { showHookData: boolean; id: string }) => (
<Order {...args} />
)
GetOne.argTypes = {
id: {
control: {
type: "text",
},
name: "order id",
defaultValue: "order_...",
},
}
@@ -0,0 +1,86 @@
import { Meta } from "@storybook/react"
import React from "react"
import { useProduct, useProducts } from "../src"
import Layout from "./components/Layout"
const Products = ({ showHookData, limit, offset }) => {
const data = useProducts({ limit, offset })
return (
<Layout showHookData={showHookData} data={data}>
<h3>Products</h3>
<ul>
{data?.products?.map((product) => (
<li key={product.id}>{product.title}</li>
))}
</ul>
</Layout>
)
}
const Product = ({ showHookData, id }) => {
const data = useProduct(id)
return (
<Layout showHookData={showHookData} data={data}>
<h3>Product: {id}</h3>
<div>
{data?.product?.title} - {data?.product?.handle}
</div>
</Layout>
)
}
const meta: Meta = {
title: "Products",
argTypes: {
showHookData: {
name: "Show hook data",
description:
"Whether or not story should display JSON of data returned from hook",
control: {
type: "boolean",
},
defaultValue: true,
},
},
parameters: {
controls: { expanded: true },
},
}
export default meta
export const List = (args: {
limit: number
offset: number
showHookData: boolean
}) => <Products {...args} />
List.argTypes = {
limit: {
control: {
type: "number",
},
name: "limit",
defaultValue: 5,
},
offset: {
control: {
type: "number",
},
defaultValue: 0,
},
}
export const GetOne = (args: { showHookData: boolean; id: string }) => (
<Product {...args} />
)
GetOne.argTypes = {
id: {
control: {
type: "text",
},
name: "product id",
defaultValue: "prod_",
},
}
@@ -0,0 +1,66 @@
import { Meta } from "@storybook/react"
import React from "react"
import { useRegion, useRegions } from "../src"
import Layout from "./components/Layout"
const Regions = ({ showHookData }) => {
const data = useRegions()
return (
<Layout showHookData={showHookData} data={data}>
<h3>Regions</h3>
<ul>
{data?.regions?.map((region) => (
<li key={region.id}>{region.name}</li>
))}
</ul>
</Layout>
)
}
const Region = ({ showHookData, id }) => {
const data = useRegion(id)
return (
<Layout showHookData={showHookData} data={data}>
<h3>Region: {id}</h3>
<div>
{data?.region?.name} - {data?.region?.currency_code}
</div>
</Layout>
)
}
const meta: Meta = {
title: "Regions",
argTypes: {
showHookData: {
name: "Show hook data",
description:
"Whether or not story should display JSON of data returned from hook",
control: {
type: "boolean",
},
defaultValue: true,
},
},
parameters: {
controls: { expanded: true },
},
}
export default meta
export const List = (args: { showHookData: boolean }) => <Regions {...args} />
export const GetOne = (args: { showHookData: boolean; id: string }) => (
<Region {...args} />
)
GetOne.argTypes = {
id: {
control: {
type: "text",
},
name: "region id",
defaultValue: "reg_",
},
}
@@ -0,0 +1,70 @@
import { Meta } from "@storybook/react"
import React from "react"
import { useReturnReason, useReturnReasons } from "../src"
import Layout from "./components/Layout"
const ReturnReasons = ({ showHookData }) => {
const data = useReturnReasons()
return (
<Layout showHookData={showHookData} data={data}>
<h3>Return Reasons</h3>
<ul>
{data?.return_reasons?.map((rr) => (
<li key={rr.id}>
{rr.label} - {rr.value}
</li>
))}
</ul>
</Layout>
)
}
const ReturnReason = ({ showHookData, id }) => {
const data = useReturnReason(id)
return (
<Layout showHookData={showHookData} data={data}>
<h3>Return Reason: {id}</h3>
<div>
{data?.return_reason?.label} - {data?.return_reason?.value}
</div>
</Layout>
)
}
const meta: Meta = {
title: "Return Reasons",
argTypes: {
showHookData: {
name: "Show hook data",
description:
"Whether or not story should display JSON of data returned from hook",
control: {
type: "boolean",
},
defaultValue: true,
},
},
parameters: {
controls: { expanded: true },
},
}
export default meta
export const List = (args: { showHookData: boolean }) => (
<ReturnReasons {...args} />
)
export const GetOne = (args: { showHookData: boolean; id: string }) => (
<ReturnReason {...args} />
)
GetOne.argTypes = {
id: {
control: {
type: "text",
},
name: "return reason id",
defaultValue: "rr_...",
},
}
@@ -0,0 +1,84 @@
import { Meta } from "@storybook/react"
import React from "react"
import { useCartShippingOptions, useShippingOptions } from "../src"
import Layout from "./components/Layout"
const ShippingOptions = ({ showHookData, cartId }) => {
const data = useShippingOptions(cartId)
return (
<Layout showHookData={showHookData} data={data}>
<h3>Shipping Options</h3>
<ul>
{data?.shipping_options?.map((so) => (
<li key={so.id}>
{so.name} - {so.amount}
</li>
))}
</ul>
</Layout>
)
}
const CartShippingOptions = ({ showHookData, cartId }) => {
const data = useCartShippingOptions(cartId)
return (
<Layout showHookData={showHookData} data={data}>
<h3>Cart shipping options: {cartId}</h3>
<ul>
{data?.shipping_options?.map((so) => (
<li key={so.id}>
{so.name} - {so.amount}
</li>
))}
</ul>
</Layout>
)
}
const meta: Meta = {
title: "Shipping Options",
argTypes: {
showHookData: {
name: "Show hook data",
description:
"Whether or not story should display JSON of data returned from hook",
control: {
type: "boolean",
},
defaultValue: true,
},
},
parameters: {
controls: { expanded: true },
},
}
export default meta
export const List = (args: { showHookData: boolean; cartId: string }) => (
<ShippingOptions {...args} />
)
List.argTypes = {
cartId: {
control: {
type: "text",
},
name: "cart id",
defaultValue: "cart_...",
},
}
export const GetOne = (args: { showHookData: boolean; cartId: string }) => (
<CartShippingOptions {...args} />
)
GetOne.argTypes = {
cartId: {
control: {
type: "text",
},
name: "cart id",
defaultValue: "cart_...",
},
}
@@ -0,0 +1,50 @@
import { Meta } from "@storybook/react"
import React from "react"
import { useCartSwap } from "../src"
import Layout from "./components/Layout"
const Swap = ({ showHookData, cartId }) => {
const data = useCartSwap(cartId)
return (
<Layout showHookData={showHookData} data={data}>
<h3>Cart swap: {cartId}</h3>
<div>
{data?.swap?.order_id} - {data?.swap?.cart_id}
</div>
</Layout>
)
}
const meta: Meta = {
title: "Swaps",
argTypes: {
showHookData: {
name: "Show hook data",
description:
"Whether or not story should display JSON of data returned from hook",
control: {
type: "boolean",
},
defaultValue: true,
},
},
parameters: {
controls: { expanded: true },
},
}
export default meta
export const GetOne = (args: { showHookData: boolean; cartId: string }) => (
<Swap {...args} />
)
GetOne.argTypes = {
cartId: {
control: {
type: "text",
},
name: "cart id",
defaultValue: "cart_...",
},
}
@@ -0,0 +1,16 @@
import React from "react"
import ReactJson from "react-json-view"
const Layout = ({ children, showHookData, data }) => {
return (
<div style={{ display: "grid", gridTemplateColumns: "50% 50%" }}>
<div>{children}</div>
<div style={{ overflowX: "auto" }}>
<h3>Raw</h3>
{showHookData && <ReactJson src={data} collapsed={true} />}
</div>
</div>
)
}
export default Layout
@@ -0,0 +1,196 @@
import { useCart } from "../../src"
import { act, renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../mocks/data"
import { createCartWrapper } from "../utils"
import { Cart } from "../../src/types"
describe("useBag hook", () => {
describe("sets a cart", () => {
test("success", async () => {
const { result } = renderHook(() => useCart(), {
wrapper: createCartWrapper(),
})
const { setCart } = result.current
act(() => {
setCart(fixtures.get("cart") as unknown as Cart)
})
const { cart, totalItems } = result.current
expect(cart).toEqual(fixtures.get("cart"))
expect(totalItems).toEqual(0)
})
})
describe("createCart", () => {
test("creates a cart", async () => {
const { result, waitFor } = renderHook(() => useCart(), {
wrapper: createCartWrapper(),
})
const { createCart } = result.current
act(() => {
createCart.mutate({})
})
await waitFor(() => result.current.createCart.isSuccess)
const { cart, totalItems } = result.current
expect(cart).toEqual(fixtures.get("cart"))
expect(totalItems).toEqual(0)
})
})
describe("startCheckout", () => {
test("creates a payment session and updates the cart", async () => {
const { result, waitFor } = renderHook(() => useCart(), {
wrapper: createCartWrapper(),
initialProps: {
initialCartState: {
...fixtures.get("cart"),
id: "test-cart",
} as unknown as Cart,
},
})
const { startCheckout } = result.current
act(() => {
startCheckout.mutate()
})
await waitFor(() => result.current.startCheckout.isSuccess)
const { cart, totalItems } = result.current
expect(cart).toEqual({
...fixtures.get("cart"),
id: "test-cart",
})
expect(totalItems).toEqual(0)
})
})
describe("updateCart", () => {
test("updates the cart", async () => {
const { result, waitFor } = renderHook(() => useCart(), {
wrapper: createCartWrapper(),
initialProps: {
initialCartState: {
...fixtures.get("cart"),
id: "test-cart",
} as unknown as Cart,
},
})
const { updateCart } = result.current
act(() => {
updateCart.mutate({
email: "zak@test.com",
})
})
await waitFor(() => result.current.updateCart.isSuccess)
const { cart, totalItems } = result.current
expect(cart).toEqual({
...fixtures.get("cart"),
id: "test-cart",
email: "zak@test.com",
})
expect(totalItems).toEqual(0)
})
})
describe("addShippingMethod", () => {
test("adds a shipping method and updates the cart", async () => {
const { result, waitFor } = renderHook(() => useCart(), {
wrapper: createCartWrapper(),
initialProps: {
initialCartState: {
...fixtures.get("cart"),
id: "test-cart",
} as unknown as Cart,
},
})
const { addShippingMethod } = result.current
act(() => {
addShippingMethod.mutate({
option_id: "test-option",
})
})
await waitFor(() => result.current.addShippingMethod.isSuccess)
const { cart, totalItems } = result.current
expect(cart).toEqual({
...fixtures.get("cart"),
id: "test-cart",
})
expect(totalItems).toEqual(0)
})
})
describe("pay", () => {
test("sets a payment session and updates the cart", async () => {
const { result, waitFor } = renderHook(() => useCart(), {
wrapper: createCartWrapper(),
initialProps: {
initialCartState: {
...fixtures.get("cart"),
id: "test-cart",
} as unknown as Cart,
},
})
const { pay } = result.current
act(() => {
pay.mutate({
provider_id: "test-provider",
})
})
await waitFor(() => result.current.pay.isSuccess)
const { cart, totalItems } = result.current
expect(cart).toEqual({
...fixtures.get("cart"),
id: "test-cart",
})
expect(totalItems).toEqual(0)
})
})
describe("completeCheckout", () => {
test("calls complete cart, does not update the cart, and returns an order", async () => {
const { result, waitFor } = renderHook(() => useCart(), {
wrapper: createCartWrapper(),
initialProps: {
initialCartState: fixtures.get("cart") as unknown as Cart,
},
})
const { completeCheckout } = result.current
act(() => {
completeCheckout.mutate()
})
await waitFor(() => result.current.completeCheckout.isSuccess)
const { cart, totalItems } = result.current
expect(cart).toEqual(fixtures.get("cart"))
expect(totalItems).toEqual(0)
expect(result.current.completeCheckout.data.type).toEqual("order")
expect(result.current.completeCheckout.data.data).toEqual(
fixtures.get("order")
)
})
})
})
@@ -0,0 +1,203 @@
import {
useCreateCart,
useUpdateCart,
useCompleteCart,
useCreatePaymentSession,
useUpdatePaymentSession,
useRefreshPaymentSession,
useSetPaymentSession,
useDeletePaymentSession,
useAddShippingMethodToCart,
} from "./../../../src/"
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../mocks/data"
import { createWrapper } from "../../utils"
describe("useCreateCart hook", () => {
test("creates a cart", async () => {
const { result, waitFor } = renderHook(() => useCreateCart(), {
wrapper: createWrapper(),
})
result.current.mutate({})
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.cart).toEqual(fixtures.get("cart"))
})
})
describe("useUpdateCart hook", () => {
test("updates a cart", async () => {
const { result, waitFor } = renderHook(() => useUpdateCart("some-cart"), {
wrapper: createWrapper(),
})
result.current.mutate({
email: "new@email.com",
})
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.cart).toEqual({
...fixtures.get("cart"),
id: "some-cart",
email: "new@email.com",
})
})
})
describe("useCompleteCart hook", () => {
test("completes a cart", async () => {
const { result, waitFor } = renderHook(() => useCompleteCart("test-cart"), {
wrapper: createWrapper(),
})
result.current.mutate()
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.type).toEqual("order")
expect(result.current.data.data).toEqual(fixtures.get("order"))
})
})
describe("useCreatePaymentSession hook", () => {
test("creates a payment session", async () => {
const { result, waitFor } = renderHook(
() => useCreatePaymentSession("test-cart"),
{
wrapper: createWrapper(),
}
)
result.current.mutate()
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.cart).toEqual({
...fixtures.get("cart"),
id: "test-cart",
})
})
})
describe("useUpdatePaymentSession hook", () => {
test("updates a payment session", async () => {
const { result, waitFor } = renderHook(
() => useUpdatePaymentSession("test-cart"),
{
wrapper: createWrapper(),
}
)
result.current.mutate({
data: {},
provider_id: "stripe",
})
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.cart).toEqual({
...fixtures.get("cart"),
id: "test-cart",
})
})
})
describe("useRefreshPaymentSession hook", () => {
test("refreshes a payment session", async () => {
const { result, waitFor } = renderHook(
() => useRefreshPaymentSession("test-cart"),
{
wrapper: createWrapper(),
}
)
result.current.mutate({
provider_id: "stripe",
})
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.cart).toEqual({
...fixtures.get("cart"),
id: "test-cart",
})
})
})
describe("useSetPaymentSession hook", () => {
test("sets a payment session", async () => {
const { result, waitFor } = renderHook(
() => useSetPaymentSession("test-cart"),
{
wrapper: createWrapper(),
}
)
result.current.mutate({
provider_id: "stripe",
})
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.cart).toEqual({
...fixtures.get("cart"),
id: "test-cart",
})
})
})
describe("useDeletePaymentSession hook", () => {
test("deletes a payment session", async () => {
const { result, waitFor } = renderHook(
() => useDeletePaymentSession("test-cart"),
{
wrapper: createWrapper(),
}
)
result.current.mutate({
provider_id: "stripe",
})
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.cart).toEqual({
...fixtures.get("cart"),
id: "test-cart",
})
})
})
describe("useAddShippingMethodToCart hook", () => {
test("adds a shipping method to a cart", async () => {
const { result, waitFor } = renderHook(
() => useAddShippingMethodToCart("test-cart"),
{
wrapper: createWrapper(),
}
)
result.current.mutate({
option_id: "test-option",
})
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.cart).toEqual({
...fixtures.get("cart"),
id: "test-cart",
})
})
})
@@ -0,0 +1,18 @@
import { useGetCart } from "./../../../src/"
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../mocks/data"
import { createWrapper } from "../../utils"
describe("useGetCart hook", () => {
test("returns a cart", async () => {
const cart = fixtures.get("cart")
const { result, waitFor } = renderHook(() => useGetCart(cart.id), {
wrapper: createWrapper(),
})
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.cart).toEqual(cart)
})
})
@@ -0,0 +1,32 @@
import { useCollections, useCollection } from "./../../../src/"
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../mocks/data"
import { createWrapper } from "../../utils"
describe("useCollections hook", () => {
test("returns a list of collections", async () => {
const collections = fixtures.list("product_collection")
const { result, waitFor } = renderHook(() => useCollections(), {
wrapper: createWrapper(),
})
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.collections).toEqual(collections)
})
})
describe("useCollection hook", () => {
test("returns a collection", async () => {
const collection = fixtures.get("product_collection")
const { result, waitFor } = renderHook(() => useCollection(collection.id), {
wrapper: createWrapper(),
})
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.collection).toEqual(collection)
})
})
@@ -0,0 +1,59 @@
import { useCreateCustomer, useUpdateMe } from "./../../../src/"
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../mocks/data"
import { createWrapper } from "../../utils"
describe("useCreateCustomer hook", () => {
test("creates a new customer", async () => {
const customer = {
first_name: "john",
last_name: "wick",
email: "johnwick@medusajs.com",
password: "supersecret",
phone: "111111",
}
const { result, waitFor } = renderHook(() => useCreateCustomer(), {
wrapper: createWrapper(),
})
result.current.mutate(customer)
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.customer).toEqual({
...fixtures.get("customer"),
...customer,
})
})
})
describe("useUpdateMe hook", () => {
test("updates current customer", async () => {
const customer = {
first_name: "lebron",
last_name: "james",
email: "lebronjames@medusajs.com",
password: "supersecret",
phone: "111111",
}
const { result, waitFor } = renderHook(() => useUpdateMe(), {
wrapper: createWrapper(),
})
result.current.mutate({
id: "cus_test",
...customer,
})
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.customer).toEqual({
...fixtures.get("customer"),
...customer,
})
})
})
@@ -0,0 +1,89 @@
import { rest } from "msw"
import { server } from "./../../../mocks/server"
import { useMeCustomer, useCustomerOrders } from "./../../../src/"
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../mocks/data"
import { createWrapper } from "../../utils"
describe("useMeCustomer hook", () => {
test("returns customer", async () => {
const customer = fixtures.get("customer")
const { result, waitFor } = renderHook(() => useMeCustomer(), {
wrapper: createWrapper(),
})
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.customer).toEqual(customer)
})
})
describe("useCustomerOrders hook", () => {
test("returns customer's orders", async () => {
const orders = fixtures.list("order", 5)
const { result, waitFor } = renderHook(() => useCustomerOrders(), {
wrapper: createWrapper(),
})
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.orders).toEqual(orders)
expect(result.current.limit).toEqual(5)
expect(result.current.offset).toEqual(0)
})
test("propagates query params and returns customer's orders", async () => {
const orders = fixtures.list("order")
server.use(
rest.get("/store/customers/me/orders", (req, res, ctx) => {
const limit = req.url.searchParams.get("limit")
const offset = req.url.searchParams.get("offset")
const expand = req.url.searchParams.get("expand")
const fields = req.url.searchParams.get("fields")
expect({
limit,
offset,
expand,
fields,
}).toEqual({
limit: "2",
offset: "5",
expand: "relation_1,relation_2",
fields: "field_1,field_2",
})
return res(
ctx.status(200),
ctx.json({
orders,
limit: 2,
offset: 5,
})
)
})
)
const { result, waitFor } = renderHook(
() =>
useCustomerOrders({
limit: 2,
offset: 5,
expand: "relation_1,relation_2",
fields: "field_1,field_2",
}),
{
wrapper: createWrapper(),
}
)
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.orders).toEqual(orders)
expect(result.current.limit).toEqual(2)
expect(result.current.offset).toEqual(5)
})
})
@@ -0,0 +1,18 @@
import { useGiftCard } from "./../../../src/"
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../mocks/data"
import { createWrapper } from "../../utils"
describe("useGiftCard hook", () => {
test("returns a gift card", async () => {
const giftCard = fixtures.get("gift_card")
const { result, waitFor } = renderHook(() => useGiftCard(giftCard.id), {
wrapper: createWrapper(),
})
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.gift_card).toEqual(giftCard)
})
})
@@ -0,0 +1,89 @@
import {
useCreateLineItem,
useUpdateLineItem,
useDeleteLineItem,
} from "./../../../src/"
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../mocks/data"
import { createWrapper } from "../../utils"
describe("useCreateLineItem hook", () => {
test("creates a line item", async () => {
const lineItem = {
variant_id: "test-variant",
quantity: 1,
}
const { result, waitFor } = renderHook(
() => useCreateLineItem("test-cart"),
{
wrapper: createWrapper(),
}
)
result.current.mutate(lineItem)
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.cart.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
...lineItem,
}),
])
)
})
})
describe("useUpdateLineItem hook", () => {
test("updates a line item", async () => {
const lineItem = {
lineId: "some-item-id",
quantity: 3,
}
const { result, waitFor } = renderHook(
() => useUpdateLineItem("test-cart"),
{
wrapper: createWrapper(),
}
)
result.current.mutate(lineItem)
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.cart.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: lineItem.lineId,
quantity: lineItem.quantity,
}),
])
)
})
})
describe("useDeleteLineItem hook", () => {
test("deletes a line item", async () => {
const lineItem = {
lineId: "some-item-id",
}
const { result, waitFor } = renderHook(
() => useDeleteLineItem("test-cart"),
{
wrapper: createWrapper(),
}
)
result.current.mutate(lineItem)
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.cart).toEqual(fixtures.get("cart"))
})
})
@@ -0,0 +1,79 @@
import { useOrderLookup } from "./../../../src/hooks/orders/queries"
import { rest } from "msw"
import { server } from "./../../../mocks/server"
import { useCartOrder, useOrder } from "./../../../src/"
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../mocks/data"
import { createWrapper } from "../../utils"
describe("useOrder hook", () => {
test("returns an order", async () => {
const order = fixtures.get("order")
const { result, waitFor } = renderHook(() => useOrder(order.id), {
wrapper: createWrapper(),
})
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.order).toEqual(order)
})
})
describe("useCartOrder hook", () => {
test("returns a cart order", async () => {
const order = fixtures.get("order")
const { result, waitFor } = renderHook(() => useCartOrder("test_cart"), {
wrapper: createWrapper(),
})
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.order).toEqual(order)
})
})
describe("useOrderLookup hook", () => {
test("propagates the query params and returns an order", async () => {
const order = fixtures.get("order")
const displayId = 400,
emailParam = "customer@test.com"
server.use(
rest.get("/store/orders", (req, res, ctx) => {
const display_id = req.url.searchParams.get("display_id")
const email = req.url.searchParams.get("email")
expect({
display_id,
email,
}).toEqual({
email: emailParam,
display_id: displayId.toString(),
})
return res(
ctx.status(200),
ctx.json({
order,
})
)
})
)
const { result, waitFor } = renderHook(
() =>
useOrderLookup({
display_id: displayId,
email: emailParam,
}),
{
wrapper: createWrapper(),
}
)
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.order).toEqual(order)
})
})
@@ -0,0 +1,53 @@
import { renderHook } from "@testing-library/react-hooks"
import { useProducts, useProduct } from "../../../src"
import { createWrapper } from "../../utils"
import { fixtures } from "../../../mocks/data/index"
describe("useProducts hook", () => {
test("gets a list of products", async () => {
const { result, waitFor } = renderHook(() => useProducts(), {
wrapper: createWrapper(),
})
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.products).toEqual(fixtures.list("product"))
})
test("gets a list of products based on limit and offset", async () => {
const { result, waitFor } = renderHook(
() =>
useProducts({
limit: 2,
offset: 5,
}),
{
wrapper: createWrapper(),
}
)
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.products).toEqual(fixtures.list("product"))
expect(result.current.limit).toEqual(2)
expect(result.current.offset).toEqual(5)
})
})
describe("useProducts hook", () => {
test("success", async () => {
const { result, waitFor } = renderHook(
() => useProduct("prod_01F0YESHQ27Y31CAMD0NV6W9YP"),
{
wrapper: createWrapper(),
}
)
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.product).toEqual(fixtures.get("product"))
})
})
@@ -0,0 +1,32 @@
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../mocks/data"
import { createWrapper } from "../../utils"
import { useRegion, useRegions } from "../../../src/"
describe("useRegions hook", () => {
test("success", async () => {
const regions = fixtures.list("region")
const { result, waitFor } = renderHook(() => useRegions(), {
wrapper: createWrapper(),
})
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.regions).toEqual(regions)
})
})
describe("useRegion hook", () => {
test("success", async () => {
const region = fixtures.get("region")
const { result, waitFor } = renderHook(() => useRegion(region.id), {
wrapper: createWrapper(),
})
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.region).toEqual(region)
})
})
@@ -0,0 +1,35 @@
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../mocks/data"
import { createWrapper } from "../../utils"
import { useReturnReason, useReturnReasons } from "../../../src/"
describe("useReturnReasons hook", () => {
test("returns a list of return reasons", async () => {
const return_reasons = fixtures.list("return_reason")
const { result, waitFor } = renderHook(() => useReturnReasons(), {
wrapper: createWrapper(),
})
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.return_reasons).toEqual(return_reasons)
})
})
describe("useReturnReason hook", () => {
test("returns a return reason", async () => {
const return_reason = fixtures.get("return_reason")
const { result, waitFor } = renderHook(
() => useReturnReason(return_reason.id),
{
wrapper: createWrapper(),
}
)
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.return_reason).toEqual(return_reason)
})
})
@@ -0,0 +1,34 @@
import { renderHook } from "@testing-library/react-hooks"
import { useCreateReturn } from "../../../src"
import { fixtures } from "../../../mocks/data"
import { createWrapper } from "../../utils"
describe("useCreateReturn hook", () => {
test("creates a return", async () => {
const ret = {
order_id: "order_38",
items: [
{
item_id: "test-item",
quantity: 1,
},
],
}
const { result, waitFor } = renderHook(() => useCreateReturn(), {
wrapper: createWrapper(),
})
result.current.mutate(ret)
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.return).toEqual(
expect.objectContaining({
...fixtures.get("return"),
order_id: ret.order_id,
})
)
})
})
@@ -0,0 +1,83 @@
import { rest } from "msw"
import { server } from "./../../../mocks/server"
import { useShippingOptions, useCartShippingOptions } from "../../../src/"
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../mocks/data"
import { createWrapper } from "../../utils"
describe("useShippingOptions hook", () => {
test("returns a list of shipping options", async () => {
const shippingOptions = fixtures.list("shipping_option", 5)
const { result, waitFor } = renderHook(() => useShippingOptions(), {
wrapper: createWrapper(),
})
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.shipping_options).toEqual(shippingOptions)
})
test("when shipping options params are provided, then they should be sent as query params", async () => {
const shippingOptions = fixtures.list("shipping_option")
server.use(
rest.get("/store/shipping-options/", (req, res, ctx) => {
const product_ids = req.url.searchParams.get("product_ids")
const is_return = req.url.searchParams.get("is_return")
const region_id = req.url.searchParams.get("region_id")
expect({
product_ids,
is_return,
region_id,
}).toEqual({
product_ids: "1,2,3",
is_return: "false",
region_id: "test-region",
})
return res(
ctx.status(200),
ctx.json({
shipping_options: fixtures.list("shipping_option"),
})
)
})
)
const { result, waitFor } = renderHook(
() =>
useShippingOptions({
product_ids: "1,2,3",
is_return: "false",
region_id: "test-region",
}),
{
wrapper: createWrapper(),
}
)
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.shipping_options).toEqual(shippingOptions)
})
})
describe("useCartShippingOptions hook", () => {
test("success", async () => {
const cartShippingOptions = fixtures.list("shipping_option")
const { result, waitFor } = renderHook(
() => useCartShippingOptions("cart_test"),
{
wrapper: createWrapper(),
}
)
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.shipping_options).toEqual(cartShippingOptions)
})
})
@@ -0,0 +1,40 @@
import { useCreateSwap } from "./../../../src/"
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../mocks/data"
import { createWrapper } from "../../utils"
describe("useCreateSwap hook", () => {
test("creates a return", async () => {
const swap = {
order_id: "order_test",
additional_items: [
{
variant_id: "new-item",
quantity: 1,
},
],
return_items: [
{
item_id: "return-item",
quantity: 1,
},
],
}
const { result, waitFor } = renderHook(() => useCreateSwap(), {
wrapper: createWrapper(),
})
result.current.mutate(swap)
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.swap).toEqual(
expect.objectContaining({
...fixtures.get("swap"),
order_id: swap.order_id,
})
)
})
})
@@ -0,0 +1,18 @@
import { useCartSwap } from "./../../../src/"
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../mocks/data"
import { createWrapper } from "../../utils"
describe("useCartSwap hook", () => {
test("returns a swap", async () => {
const swap = fixtures.get("swap")
const { result, waitFor } = renderHook(() => useCartSwap("cart_test"), {
wrapper: createWrapper(),
})
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.swap).toEqual(swap)
})
})
@@ -0,0 +1,322 @@
import { generateCartState } from "../../src/contexts/session-cart"
import { ProductVariant } from "@medusajs/medusa"
import { useSessionCart } from "../../src"
import { act, renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../mocks/data"
import { createSessionCartWrapper } from "../utils"
const initialSessionCartState = {
region: fixtures.get("region"),
totalItems: 0,
total: 0,
items: [],
}
describe("useSessionCart hook", () => {
describe("sets a region", () => {
test("success", async () => {
const { result } = renderHook(() => useSessionCart(), {
wrapper: createSessionCartWrapper(),
})
const { setRegion } = result.current
act(() => {
setRegion(fixtures.get("region"))
})
const { region, total, totalItems } = result.current
expect(region).toEqual(fixtures.get("region"))
expect(total).toEqual(0)
expect(totalItems).toEqual(0)
})
})
describe("item operations", () => {
test("addItem", () => {
const { result } = renderHook(() => useSessionCart(), {
wrapper: createSessionCartWrapper(),
initialProps: {
initialState: initialSessionCartState,
},
})
const { addItem } = result.current
const variant = fixtures.get("product_variant")
act(() => {
addItem({
variant: variant as unknown as ProductVariant,
quantity: 1,
})
})
const { items, totalItems, total } = result.current
expect(totalItems).toBe(1)
expect(total).toBe(1000)
expect(items).toEqual([
{
variant: expect.objectContaining(variant),
quantity: 1,
total: 1000,
},
])
})
test("updateItem", () => {
const variant = fixtures.get("product_variant")
const { result } = renderHook(() => useSessionCart(), {
wrapper: createSessionCartWrapper(),
initialProps: {
initialState: generateCartState(initialSessionCartState, [
{
variant: variant as unknown as ProductVariant,
quantity: 1,
},
{
variant: {
...variant,
id: "test-variant",
} as unknown as ProductVariant,
quantity: 1,
},
]),
},
})
const { updateItem } = result.current
act(() => {
updateItem(variant.id, {
quantity: 4,
})
})
const { items, totalItems, total } = result.current
expect(totalItems).toBe(5)
expect(total).toBe(5 * 1000)
expect(items).toEqual([
{
variant: expect.objectContaining(variant),
quantity: 4,
total: 1000,
},
{
variant: {
...variant,
id: "test-variant",
},
quantity: 1,
total: 1000,
},
])
})
test("removeItem", () => {
const variant = fixtures.get("product_variant")
const { result } = renderHook(() => useSessionCart(), {
wrapper: createSessionCartWrapper(),
initialProps: {
initialState: generateCartState(initialSessionCartState, [
{
variant: variant as unknown as ProductVariant,
quantity: 3,
},
{
variant: {
...variant,
id: "test-variant",
} as unknown as ProductVariant,
quantity: 1,
},
]),
},
})
const { removeItem } = result.current
act(() => {
removeItem(variant.id)
})
const { items, totalItems, total } = result.current
expect(totalItems).toBe(1)
expect(total).toBe(1000)
expect(items).toEqual([
{
variant: {
...variant,
id: "test-variant",
},
quantity: 1,
total: 1000,
},
])
})
test("incrementItemQuantity", () => {
const variant = fixtures.get("product_variant")
const { result } = renderHook(() => useSessionCart(), {
wrapper: createSessionCartWrapper(),
initialProps: {
initialState: generateCartState(initialSessionCartState, [
{
variant: variant as unknown as ProductVariant,
quantity: 2,
},
]),
},
})
const { incrementItemQuantity } = result.current
act(() => {
incrementItemQuantity(variant.id)
})
const { items, totalItems, total } = result.current
expect(totalItems).toBe(3)
expect(total).toBe(3 * 1000)
expect(items).toEqual([
{
variant,
quantity: 3,
total: 1000,
},
])
})
test("decrementItemQuantity", () => {
const variant = fixtures.get("product_variant")
const { result } = renderHook(() => useSessionCart(), {
wrapper: createSessionCartWrapper(),
initialProps: {
initialState: generateCartState(initialSessionCartState, [
{
variant: variant as unknown as ProductVariant,
quantity: 4,
},
]),
},
})
const { decrementItemQuantity } = result.current
act(() => {
decrementItemQuantity(variant.id)
})
const { items, totalItems, total } = result.current
expect(totalItems).toBe(3)
expect(total).toBe(3 * 1000)
expect(items).toEqual([
{
variant,
quantity: 3,
total: 1000,
},
])
})
test("setItems", () => {
const variant = fixtures.get("product_variant")
const { result } = renderHook(() => useSessionCart(), {
wrapper: createSessionCartWrapper(),
initialProps: {
initialState: generateCartState(initialSessionCartState, [
{
variant: variant as unknown as ProductVariant,
quantity: 4,
},
]),
},
})
const { setItems } = result.current
act(() => {
setItems([
{
variant: {
...variant,
id: "test-variant",
} as unknown as ProductVariant,
quantity: 1,
},
])
})
const { items, totalItems, total } = result.current
expect(totalItems).toBe(1)
expect(total).toBe(1000)
expect(items).toEqual([
{
variant: expect.objectContaining({
id: "test-variant",
}),
quantity: 1,
total: 1000,
},
])
})
test("getItem", () => {
const variant = fixtures.get("product_variant")
const { result } = renderHook(() => useSessionCart(), {
wrapper: createSessionCartWrapper(),
initialProps: {
initialState: generateCartState(initialSessionCartState, [
{
variant: variant as unknown as ProductVariant,
quantity: 1,
},
]),
},
})
const { getItem } = result.current
let item
act(() => {
item = getItem(variant.id)
})
expect(item).toEqual({
variant,
quantity: 1,
total: 1000,
})
})
test("clearItems", () => {
const variant = fixtures.get("product_variant")
const { result } = renderHook(() => useSessionCart(), {
wrapper: createSessionCartWrapper(),
initialProps: {
initialState: generateCartState(initialSessionCartState, [
{
variant: variant as unknown as ProductVariant,
quantity: 4,
},
]),
},
})
const { clearItems } = result.current
act(() => {
clearItems()
})
const { items, totalItems, total } = result.current
expect(totalItems).toBe(0)
expect(total).toBe(0)
expect(items).toEqual([])
})
})
})
+72
View File
@@ -0,0 +1,72 @@
import * as React from "react"
import { QueryClient } from "react-query"
import {
SessionCartProvider,
SessionCartState,
CartProvider,
MedusaProvider,
} from "../src"
import { Cart } from "../src/types"
const createTestQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
export function createWrapper() {
const qc = createTestQueryClient()
return ({ children }) => (
<MedusaProvider queryClientProviderProps={{ client: qc }} baseUrl="">
{children}
</MedusaProvider>
)
}
export function createSessionCartWrapper() {
const qc = createTestQueryClient()
return ({
children,
initialState,
}: {
initialState: SessionCartState
children?: React.ReactNode
}) => {
return (
<MedusaProvider queryClientProviderProps={{ client: qc }} baseUrl="">
<SessionCartProvider initialState={initialState}>
{children}
</SessionCartProvider>
</MedusaProvider>
)
}
}
export function createCartWrapper() {
const qc = createTestQueryClient()
return ({
children,
initialSessionCartState,
initialCartState,
}: {
initialSessionCartState?: SessionCartState
initialCartState?: Cart
children?: React.ReactNode
}) => {
return (
<MedusaProvider queryClientProviderProps={{ client: qc }} baseUrl="">
<SessionCartProvider initialState={initialSessionCartState}>
<CartProvider initialState={initialCartState}>
{children}
</CartProvider>
</SessionCartProvider>
</MedusaProvider>
)
}
}
@@ -0,0 +1,169 @@
import { RegionInfo, ProductVariantInfo } from "./../../src/types"
import { fixtures } from "./../../mocks/data/"
import {
computeVariantPrice,
getVariantPrice,
computeAmount,
formatAmount,
formatVariantPrice,
} from "./../../src/"
describe("getVariantPrice", () => {
test("finds the variant price and returns its amount", () => {
const variant = fixtures.get("product_variant")
const region = fixtures.get("region")
const amount = getVariantPrice(
variant as unknown as ProductVariantInfo,
region
)
expect(amount).toEqual(1000)
})
test("when no region is provided, then it should return 0", () => {
const variant = fixtures.get("product_variant")
const amount = getVariantPrice(
variant as unknown as ProductVariantInfo,
{} as RegionInfo
)
expect(amount).toEqual(0)
})
test("when no product variant is provided, then it should return 0", () => {
const region = fixtures.get("region")
const amount = getVariantPrice({} as ProductVariantInfo, region)
expect(amount).toEqual(0)
})
test("when no product variant and region are provided, then it should return 0", () => {
const amount = getVariantPrice({} as ProductVariantInfo, {} as RegionInfo)
expect(amount).toEqual(0)
})
})
describe("computeAmount", () => {
test("given an amount and a region, it should return a decimal amount not including taxes", () => {
const region = fixtures.get("region")
const amount = computeAmount({ amount: 3000, region, includeTaxes: false })
expect(amount).toEqual(30)
})
test("given an amount and a region, it should return a decimal amount including taxes", () => {
const region = fixtures.get("region")
const amount = computeAmount({
amount: 3000,
region: {
...region,
tax_rate: 10,
},
})
expect(amount).toEqual(33)
})
test("when no region is provided, then it should return the decimal amount", () => {
const region = fixtures.get("region")
const amount = computeAmount({ amount: 2000, region })
expect(amount).toEqual(20)
})
})
describe("computeVariantPrice", () => {
test("finds the variant price and returns a decimal amount not including taxes", () => {
const variant = fixtures.get("product_variant")
const region = fixtures.get("region")
const price = computeVariantPrice({
variant: variant as unknown as ProductVariantInfo,
region,
})
expect(price).toEqual(10)
})
test("finds the variant price and returns a decimal amount including taxes", () => {
const variant = fixtures.get("product_variant")
const region = fixtures.get("region")
const price = computeVariantPrice({
variant: variant as unknown as ProductVariantInfo,
region: {
...region,
tax_rate: 15,
},
includeTaxes: true,
})
expect(price).toEqual(11.5)
})
})
describe("formatVariantPrice", () => {
test("given a variant and region, should return a decimal localized amount including taxes and the region's currency code", () => {
const region = fixtures.get("region")
const variant = fixtures.get("product_variant")
const price = formatVariantPrice({
variant: variant as unknown as ProductVariantInfo,
region: {
...region,
tax_rate: 15,
},
})
expect(price).toEqual("$11.50")
})
test("given a variant, region, and 1 digit, should return a decimal (1 fraction digit) localized amount including taxes and the region's currency code", () => {
const region = fixtures.get("region")
const variant = fixtures.get("product_variant")
const price = formatVariantPrice({
variant: variant as unknown as ProductVariantInfo,
region: {
...region,
tax_rate: 15,
},
maximumFractionDigits: 1,
})
expect(price).toEqual("$11.5")
})
test("given a variant, region, and a custom locale, should return a decimal localized amount including taxes and the region's currency code", () => {
const region = fixtures.get("region")
const variant = fixtures.get("product_variant")
const price = formatVariantPrice({
variant: variant as unknown as ProductVariantInfo,
region: {
...region,
tax_rate: 15,
},
locale: "fr-FR",
})
expect(price.replace(/\s/, " ")).toEqual("11,50 $US")
})
})
describe("formatAmount", () => {
test("given an amount and region, should return a decimal localized amount including taxes and the region's currency code", () => {
const region = fixtures.get("region")
const price = formatAmount({
amount: 3000,
region: {
...region,
tax_rate: 15,
},
})
expect(price).toEqual("$34.50")
})
test("given an amount and no region, should return a decimal localized amount", () => {
const price = formatAmount({ amount: 3000, region: {} as RegionInfo })
expect(price).toEqual("30")
})
})
+35
View File
@@ -0,0 +1,35 @@
{
// see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
"include": ["src", "types"],
"compilerOptions": {
"module": "esnext",
"lib": ["dom", "esnext"],
"importHelpers": true,
// output .d.ts declaration files for consumers
"declaration": true,
// output .js.map sourcemap files for consumers
"sourceMap": true,
// match output dir to input dir. e.g. dist/index instead of dist/src/index
"rootDir": "./src",
// stricter type-checking for stronger correctness. Recommended by TS
"strict": true,
// linter checks for common issues
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
// noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative
"noUnusedLocals": true,
"noUnusedParameters": true,
// use Node's module resolution algorithm, instead of the legacy TS one
"moduleResolution": "node",
// transpile JSX to React.createElement
"jsx": "react",
// interop between ESM and CJS modules. Recommended by TS
"esModuleInterop": true,
// significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS
"skipLibCheck": true,
// error out if import and file system have a casing mismatch. Recommended by TS
"forceConsistentCasingInFileNames": true,
// `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc`
"noEmit": true,
}
}
File diff suppressed because it is too large Load Diff
-53
View File
@@ -857,18 +857,6 @@
exec-sh "^0.3.2"
minimist "^1.2.0"
"@hapi/hoek@^9.0.0":
version "9.2.0"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.0.tgz#f3933a44e365864f4dad5db94158106d511e8131"
integrity sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug==
"@hapi/topo@^5.0.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012"
integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==
dependencies:
"@hapi/hoek" "^9.0.0"
"@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@@ -1070,23 +1058,6 @@
readdirp "^2.2.1"
upath "^1.1.1"
"@sideway/address@^4.1.0":
version "4.1.2"
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.2.tgz#811b84333a335739d3969cfc434736268170cad1"
integrity sha512-idTz8ibqWFrPU8kMirL0CoPH/A29XOzzAzpyN3zQ4kAWnzmNfFmRaoMNN6VI8ske5M73HZyhIaW4OuSFIdM4oA==
dependencies:
"@hapi/hoek" "^9.0.0"
"@sideway/formula@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
"@sideway/pinpoint@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df"
integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==
"@sinonjs/commons@^1.7.0":
version "1.8.2"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.2.tgz#858f5c4b48d80778fde4b9d541f27edc0d56488b"
@@ -3196,22 +3167,6 @@ jest@^25.5.2:
import-local "^3.0.2"
jest-cli "^25.5.4"
joi-objectid@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/joi-objectid/-/joi-objectid-3.0.1.tgz#63ace7860f8e1a993a28d40c40ffd8eff01a3668"
integrity sha512-V/3hbTlGpvJ03Me6DJbdBI08hBTasFOmipsauOsxOSnsF1blxV537WTl1zPwbfcKle4AK0Ma4OPnzMH4LlvTpQ==
joi@^17.3.0:
version "17.4.2"
resolved "https://registry.yarnpkg.com/joi/-/joi-17.4.2.tgz#02f4eb5cf88e515e614830239379dcbbe28ce7f7"
integrity sha512-Lm56PP+n0+Z2A2rfRvsfWVDXGEWjXxatPopkQ8qQ5mxCEhwHG+Ettgg5o98FFaxilOxozoa14cFhrE/hOzh/Nw==
dependencies:
"@hapi/hoek" "^9.0.0"
"@hapi/topo" "^5.0.0"
"@sideway/address" "^4.1.0"
"@sideway/formula" "^3.0.0"
"@sideway/pinpoint" "^2.0.0"
js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -3424,14 +3379,6 @@ math-random@^1.0.1:
resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c"
integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==
medusa-core-utils@^1.1.22:
version "1.1.22"
resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-1.1.22.tgz#84ce0af0a7c672191d758ea462056e30a39d08b1"
integrity sha512-kMuRkWOuNG4Bw6epg/AYu95UJuE+rjHTeTWRLbEPrYGjWREV82tLWVDI21/QcccmaHmMU98Rkw2z9JwyFZIiyw==
dependencies:
joi "^17.3.0"
joi-objectid "^3.0.1"
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
+44 -1642
View File
File diff suppressed because it is too large Load Diff
+240 -4523
View File
File diff suppressed because it is too large Load Diff