diff --git a/integration-tests/modules/__tests__/link-modules/store-currency.spec.ts b/integration-tests/modules/__tests__/link-modules/store-currency.spec.ts
new file mode 100644
index 0000000000..984485be62
--- /dev/null
+++ b/integration-tests/modules/__tests__/link-modules/store-currency.spec.ts
@@ -0,0 +1,54 @@
+import { ModuleRegistrationName } from "@medusajs/modules-sdk"
+import { ICurrencyModuleService, IStoreModuleService } from "@medusajs/types"
+import { medusaIntegrationTestRunner } from "medusa-test-utils"
+
+jest.setTimeout(50000)
+
+const env = { MEDUSA_FF_MEDUSA_V2: true }
+
+medusaIntegrationTestRunner({
+ env,
+ testSuite: ({ getContainer }) => {
+ describe("Link: Store Currency", () => {
+ let appContainer
+ let storeModuleService: IStoreModuleService
+ let currencyModuleService: ICurrencyModuleService
+ let remoteQuery
+
+ beforeAll(async () => {
+ appContainer = getContainer()
+ storeModuleService = appContainer.resolve(ModuleRegistrationName.STORE)
+ currencyModuleService = appContainer.resolve(
+ ModuleRegistrationName.CURRENCY
+ )
+ remoteQuery = appContainer.resolve("remoteQuery")
+ })
+
+ it("should query store and default currency with remote query", async () => {
+ const store = await storeModuleService.create({
+ name: "Store",
+ default_currency_code: "usd",
+ supported_currency_codes: ["usd"],
+ })
+
+ const stores = await remoteQuery({
+ store: {
+ fields: ["id"],
+ default_currency: {
+ fields: ["code"],
+ },
+ },
+ })
+
+ expect(stores).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ id: store.id,
+ default_currency: expect.objectContaining({ code: "usd" }),
+ }),
+ ])
+ )
+ })
+ })
+ },
+})
diff --git a/integration-tests/modules/__tests__/store/admin/store.spec.ts b/integration-tests/modules/__tests__/store/admin/store.spec.ts
index 149c57144b..3de41cf9a2 100644
--- a/integration-tests/modules/__tests__/store/admin/store.spec.ts
+++ b/integration-tests/modules/__tests__/store/admin/store.spec.ts
@@ -1,7 +1,7 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IStoreModuleService } from "@medusajs/types"
-import { createAdminUser } from "../../../../helpers/create-admin-user"
import { medusaIntegrationTestRunner } from "medusa-test-utils"
+import { createAdminUser } from "../../../../helpers/create-admin-user"
jest.setTimeout(50000)
@@ -57,7 +57,7 @@ medusaIntegrationTestRunner({
)
await service.delete(createdStore.id)
- const listedStores = await api.get(`/admin/stores`, adminHeaders)
+ const listedStores = await api.get(`/admin/stores?id=${createdStore.id}`, adminHeaders)
expect(listedStores.data.stores).toHaveLength(0)
})
})
diff --git a/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx b/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx
index 6c671b75af..fe80ebca1d 100644
--- a/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx
+++ b/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx
@@ -28,6 +28,9 @@ import { queryClient } from "../../../lib/medusa"
import { useSearch } from "../../../providers/search-provider"
import { useSidebar } from "../../../providers/sidebar-provider"
import { useTheme } from "../../../providers/theme-provider"
+import { useV2Session } from "../../../lib/api-v2"
+
+const V2_ENABLED = import.meta.env.VITE_MEDUSA_V2 || false
export const Shell = ({ children }: PropsWithChildren) => {
return (
@@ -116,7 +119,19 @@ const Breadcrumbs = () => {
}
const UserBadge = () => {
- const { user, isLoading, isError, error } = useAdminGetSession()
+ // Comment: Only place where we switch between the two modes inline.
+ // This is to avoid having to rebuild the shell for the app.
+ let { user, isLoading, isError, error } = {} as any
+
+ // Medusa V2 disabled
+ ;({ user, isLoading, isError, error } = useAdminGetSession({
+ enabled: V2_ENABLED == "false",
+ }))
+
+ // Medusa V2 enabled
+ ;({ user, isLoading, isError, error } = useV2Session({
+ enabled: V2_ENABLED == "true",
+ }))
const name = [user?.first_name, user?.last_name].filter(Boolean).join(" ")
const displayName = name || user?.email
diff --git a/packages/admin-next/dashboard/src/lib/api-v2/auth.ts b/packages/admin-next/dashboard/src/lib/api-v2/auth.ts
new file mode 100644
index 0000000000..c16e0657ba
--- /dev/null
+++ b/packages/admin-next/dashboard/src/lib/api-v2/auth.ts
@@ -0,0 +1,43 @@
+import { useMutation } from "@tanstack/react-query"
+import { adminAuthKeys, useAdminCustomQuery } from "medusa-react"
+import { medusa } from "../medusa"
+
+export const useV2Session = (options: any = {}) => {
+ const { data, isLoading, isError, error } = useAdminCustomQuery(
+ "/admin/users/me",
+ adminAuthKeys.details(),
+ {},
+ options
+ )
+
+ const user = data?.user
+
+ return { user, isLoading, isError, error }
+}
+
+export const useV2LoginWithSession = () => {
+ return useMutation(
+ (payload: { email: string; password: string }) =>
+ medusa.client.request("POST", "/auth/admin/emailpass", {
+ email: payload.email,
+ password: payload.password,
+ }),
+ {
+ onSuccess: async (args: { token: string }) => {
+ const { token } = args
+
+ // Convert the JWT to a session cookie
+ // TODO: Consider if the JWT is a good choice for session token
+ await medusa.client.request(
+ "POST",
+ "/auth/session",
+ {},
+ {},
+ {
+ Authorization: `Bearer ${token}`,
+ }
+ )
+ },
+ }
+ )
+}
diff --git a/packages/admin-next/dashboard/src/lib/api-v2/index.ts b/packages/admin-next/dashboard/src/lib/api-v2/index.ts
new file mode 100644
index 0000000000..4b23d2e698
--- /dev/null
+++ b/packages/admin-next/dashboard/src/lib/api-v2/index.ts
@@ -0,0 +1,2 @@
+export * from "./auth"
+export * from "./store"
diff --git a/packages/admin-next/dashboard/src/lib/api-v2/store.ts b/packages/admin-next/dashboard/src/lib/api-v2/store.ts
new file mode 100644
index 0000000000..e4537386da
--- /dev/null
+++ b/packages/admin-next/dashboard/src/lib/api-v2/store.ts
@@ -0,0 +1,14 @@
+import { adminStoreKeys, useAdminCustomQuery } from "medusa-react"
+
+export const useV2Store = ({ initialData }: { initialData?: any }) => {
+ const { data, isLoading, isError, error } = useAdminCustomQuery(
+ "/admin/stores",
+ adminStoreKeys.details(),
+ {},
+ { initialData }
+ )
+
+ const store = data.stores[0]
+
+ return { store, isLoading, isError, error }
+}
diff --git a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx
index 8c42e124a1..ed6899ce29 100644
--- a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx
+++ b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx
@@ -6,7 +6,7 @@ import {
import { v1Routes } from "./v1"
import { v2Routes } from "./v2"
-const V2_ENABLED = import.meta.env.MEDUSA_V2 || false
+const V2_ENABLED = import.meta.env.VITE_MEDUSA_V2 || false
const router = createBrowserRouter(V2_ENABLED ? v2Routes : v1Routes)
diff --git a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx
index 588e3fa6c3..15d573b178 100644
--- a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx
+++ b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx
@@ -1,4 +1,38 @@
-import { RouteObject } from "react-router-dom"
+import { Navigate, RouteObject, useLocation } from "react-router-dom"
+import { SettingsLayout } from "../../components/layout/settings-layout"
+
+import { Outlet } from "react-router-dom"
+
+import { SidebarProvider } from "../sidebar-provider"
+import { SearchProvider } from "../search-provider"
+import { ErrorBoundary } from "../../components/error/error-boundary"
+import { Spinner } from "@medusajs/icons"
+import { useV2Session } from "../../lib/api-v2"
+
+export const ProtectedRoute = () => {
+ const { user, isLoading } = useV2Session()
+ const location = useLocation()
+
+ if (isLoading) {
+ return (
+
+
+
+ )
+ }
+
+ if (!user) {
+ return
+ }
+
+ return (
+
+
+
+
+
+ )
+}
/**
* Experimental V2 routes.
@@ -10,8 +44,62 @@ export const v2Routes: RouteObject[] = [
path: "/login",
lazy: () => import("../../v2-routes/login"),
},
+ {
+ path: "/",
+ lazy: () => import("../../v2-routes/home"),
+ },
{
path: "*",
lazy: () => import("../../routes/no-match"),
},
+ {
+ element: ,
+ errorElement: ,
+ children: [
+ {
+ path: "/settings",
+ element: ,
+ handle: {
+ crumb: () => "Settings",
+ },
+ children: [
+ {
+ index: true,
+ lazy: () => import("../../v2-routes/settings"),
+ },
+ {
+ path: "profile",
+ lazy: () => import("../../v2-routes/profile/profile-detail"),
+ handle: {
+ crumb: () => "Profile",
+ },
+ children: [
+ {
+ path: "edit",
+ lazy: () => import("../../v2-routes/profile/profile-edit"),
+ },
+ ],
+ },
+ {
+ path: "store",
+ lazy: () => import("../../v2-routes/store/store-detail"),
+ handle: {
+ crumb: () => "Store",
+ },
+ children: [
+ {
+ path: "edit",
+ lazy: () => import("../../v2-routes/store/store-edit"),
+ },
+ {
+ path: "add-currencies",
+ lazy: () =>
+ import("../../v2-routes/store/store-add-currencies"),
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
]
diff --git a/packages/admin-next/dashboard/src/v2-routes/home/home.tsx b/packages/admin-next/dashboard/src/v2-routes/home/home.tsx
new file mode 100644
index 0000000000..c29fd68fc6
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/home/home.tsx
@@ -0,0 +1,13 @@
+import { useEffect } from "react"
+import { useNavigate } from "react-router-dom"
+
+export const Home = () => {
+ const navigate = useNavigate()
+
+ // Currently, the home page simply redirects to the settings page
+ useEffect(() => {
+ navigate("/settings", { replace: true })
+ }, [navigate])
+
+ return
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/home/index.ts b/packages/admin-next/dashboard/src/v2-routes/home/index.ts
new file mode 100644
index 0000000000..ea1e81bb8a
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/home/index.ts
@@ -0,0 +1 @@
+export { Home as Component } from "./home";
diff --git a/packages/admin-next/dashboard/src/v2-routes/login/login.tsx b/packages/admin-next/dashboard/src/v2-routes/login/login.tsx
index 8f14c1f8c7..bbee7dc464 100644
--- a/packages/admin-next/dashboard/src/v2-routes/login/login.tsx
+++ b/packages/admin-next/dashboard/src/v2-routes/login/login.tsx
@@ -1,3 +1,143 @@
+import { zodResolver } from "@hookform/resolvers/zod"
+import { Button, Heading, Input, Text } from "@medusajs/ui"
+import { useForm } from "react-hook-form"
+import { Trans, useTranslation } from "react-i18next"
+import { Link, useLocation, useNavigate } from "react-router-dom"
+import * as z from "zod"
+
+import { Form } from "../../components/common/form"
+import { LogoBox } from "../../components/common/logo-box"
+import { useV2LoginWithSession } from "../../lib/api-v2"
+import { isAxiosError } from "../../lib/is-axios-error"
+
+const LoginSchema = z.object({
+ email: z.string().email(),
+ password: z.string(),
+})
+
export const Login = () => {
- return Medusa V2 Login
+ const { t } = useTranslation()
+ const location = useLocation()
+ const navigate = useNavigate()
+
+ const from = location.state?.from?.pathname || "/settings"
+
+ const form = useForm>({
+ resolver: zodResolver(LoginSchema),
+ defaultValues: {
+ email: "",
+ password: "",
+ },
+ })
+
+ // TODO: Update when more than emailpass is supported
+ const { mutateAsync, isLoading } = useV2LoginWithSession()
+
+ const handleSubmit = form.handleSubmit(async ({ email, password }) => {
+ await mutateAsync(
+ {
+ email,
+ password,
+ },
+ {
+ onSuccess: () => {
+ navigate(from, { replace: true })
+ },
+ onError: (error) => {
+ if (isAxiosError(error)) {
+ if (error.response?.status === 401) {
+ form.setError("email", {
+ type: "manual",
+ })
+
+ form.setError("password", {
+ type: "manual",
+ message: t("errors.invalidCredentials"),
+ })
+
+ return
+ }
+ }
+
+ form.setError("root.serverError", {
+ type: "manual",
+ message: t("errors.serverError"),
+ })
+ },
+ }
+ )
+ })
+
+ return (
+
+
+
+
+ {t("login.title")}
+
+ {t("login.hint")}
+
+
+
+
+
+
+ ,
+ ]}
+ />
+
+
+
+ )
}
diff --git a/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/components/profile-general-section/index.ts b/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/components/profile-general-section/index.ts
new file mode 100644
index 0000000000..948c77df96
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/components/profile-general-section/index.ts
@@ -0,0 +1 @@
+export * from "./profile-general-section"
diff --git a/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/components/profile-general-section/profile-general-section.tsx b/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/components/profile-general-section/profile-general-section.tsx
new file mode 100644
index 0000000000..79e5057699
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/components/profile-general-section/profile-general-section.tsx
@@ -0,0 +1,63 @@
+import { Button, Container, Heading, StatusBadge, Text } from "@medusajs/ui"
+import { useTranslation } from "react-i18next"
+import { Link } from "react-router-dom"
+import { languages } from "../../../../../i18n/config"
+import { UserDTO } from "@medusajs/types"
+
+type ProfileGeneralSectionProps = {
+ user: Partial>
+}
+
+export const ProfileGeneralSection = ({ user }: ProfileGeneralSectionProps) => {
+ const { i18n, t } = useTranslation()
+ return (
+
+
+
+ {t("profile.domain")}
+
+ {t("profile.manageYourProfileDetails")}
+
+
+
+
+
+
+
+
+ {t("fields.name")}
+
+
+ {user.first_name} {user.last_name}
+
+
+
+
+ {t("fields.email")}
+
+
+ {user.email}
+
+
+
+
+ {t("profile.language")}
+
+
+ {languages.find((lang) => lang.code === i18n.language)
+ ?.display_name || "-"}
+
+
+
+
+ {t("profile.usageInsights")}
+
+
+ {t("general.disabled")}
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/index.ts b/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/index.ts
new file mode 100644
index 0000000000..9d981a433d
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/index.ts
@@ -0,0 +1 @@
+export { ProfileDetail as Component } from "./profile-detail"
diff --git a/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/profile-detail.tsx b/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/profile-detail.tsx
new file mode 100644
index 0000000000..2880bcff6e
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/profile-detail.tsx
@@ -0,0 +1,26 @@
+import { Outlet, json } from "react-router-dom"
+import { ProfileGeneralSection } from "./components/profile-general-section"
+import { useV2Session } from "../../../lib/api-v2"
+
+export const ProfileDetail = () => {
+ const { user, isLoading, isError, error } = useV2Session()
+
+ if (isLoading) {
+ return Loading...
+ }
+
+ if (isError || !user) {
+ if (error) {
+ throw error
+ }
+
+ throw json("An unknown error has occured", 500)
+ }
+
+ return (
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/profile/profile-edit/components/edit-profile-form/edit-profile-form.tsx b/packages/admin-next/dashboard/src/v2-routes/profile/profile-edit/components/edit-profile-form/edit-profile-form.tsx
new file mode 100644
index 0000000000..8ff62b4ee2
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/profile/profile-edit/components/edit-profile-form/edit-profile-form.tsx
@@ -0,0 +1,207 @@
+import { zodResolver } from "@hookform/resolvers/zod"
+import { Button, Input, Select, Switch } from "@medusajs/ui"
+import { adminUserKeys, useAdminCustomPost } from "medusa-react"
+import { useForm } from "react-hook-form"
+import { Trans, useTranslation } from "react-i18next"
+import * as zod from "zod"
+
+import { Form } from "../../../../../components/common/form"
+import {
+ RouteDrawer,
+ useRouteModal,
+} from "../../../../../components/route-modal"
+import { languages } from "../../../../../i18n/config"
+import { queryClient } from "../../../../../lib/medusa"
+import { UserDTO } from "@medusajs/types"
+
+type EditProfileProps = {
+ user: Partial>
+ usageInsights: boolean
+}
+
+const EditProfileSchema = zod.object({
+ first_name: zod.string().optional(),
+ last_name: zod.string().optional(),
+ language: zod.string(),
+ usage_insights: zod.boolean(),
+})
+
+export const EditProfileForm = ({ user, usageInsights }: EditProfileProps) => {
+ const { t, i18n } = useTranslation()
+ const { handleSuccess } = useRouteModal()
+
+ const form = useForm>({
+ defaultValues: {
+ first_name: user.first_name ?? "",
+ last_name: user.last_name ?? "",
+ language: i18n.language,
+ usage_insights: usageInsights,
+ },
+ resolver: zodResolver(EditProfileSchema),
+ })
+
+ const changeLanguage = (code: string) => {
+ i18n.changeLanguage(code)
+ }
+
+ const sortedLanguages = languages.sort((a, b) =>
+ a.display_name.localeCompare(b.display_name)
+ )
+
+ const { mutateAsync, isLoading } = useAdminCustomPost(
+ `/admin/users/${user.id}`,
+ [...adminUserKeys.lists(), ...adminUserKeys.detail(user.id!)]
+ )
+
+ const handleSubmit = form.handleSubmit(async (values) => {
+ await mutateAsync(
+ {
+ first_name: values.first_name,
+ last_name: values.last_name,
+ },
+ {
+ onSuccess: () => {
+ queryClient.invalidateQueries([
+ ...adminUserKeys.lists(),
+ ...adminUserKeys.detail(user.id!),
+ ])
+ },
+ onError: () => {
+ return
+ },
+ }
+ )
+
+ changeLanguage(values.language)
+
+ handleSuccess()
+ })
+
+ return (
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/profile/profile-edit/index.ts b/packages/admin-next/dashboard/src/v2-routes/profile/profile-edit/index.ts
new file mode 100644
index 0000000000..99be2393cb
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/profile/profile-edit/index.ts
@@ -0,0 +1 @@
+export { ProfileEdit as Component } from "./profile-edit"
diff --git a/packages/admin-next/dashboard/src/v2-routes/profile/profile-edit/profile-edit.tsx b/packages/admin-next/dashboard/src/v2-routes/profile/profile-edit/profile-edit.tsx
new file mode 100644
index 0000000000..ac7f42225a
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/profile/profile-edit/profile-edit.tsx
@@ -0,0 +1,26 @@
+import { Heading } from "@medusajs/ui"
+import { useTranslation } from "react-i18next"
+import { RouteDrawer } from "../../../components/route-modal"
+import { EditProfileForm } from "./components/edit-profile-form/edit-profile-form"
+import { useV2Session } from "../../../lib/api-v2"
+
+export const ProfileEdit = () => {
+ const { user, isLoading, isError, error } = useV2Session()
+
+ const { t } = useTranslation()
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+
+ {t("profile.editProfile")}
+
+ {!isLoading && user && (
+
+ )}
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/settings/index.ts b/packages/admin-next/dashboard/src/v2-routes/settings/index.ts
new file mode 100644
index 0000000000..af259dd1e4
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/settings/index.ts
@@ -0,0 +1 @@
+export { Settings as Component } from "./settings";
diff --git a/packages/admin-next/dashboard/src/v2-routes/settings/settings.tsx b/packages/admin-next/dashboard/src/v2-routes/settings/settings.tsx
new file mode 100644
index 0000000000..058b328f7c
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/settings/settings.tsx
@@ -0,0 +1,15 @@
+import { useEffect } from "react"
+import { Outlet, useLocation, useNavigate } from "react-router-dom"
+
+export const Settings = () => {
+ const navigate = useNavigate()
+ const location = useLocation()
+
+ useEffect(() => {
+ if (location.pathname === "/settings") {
+ navigate("/settings/profile")
+ }
+ }, [location.pathname, navigate])
+
+ return
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-add-currencies/components/add-currencies-form/add-currencies-form.tsx b/packages/admin-next/dashboard/src/v2-routes/store/store-add-currencies/components/add-currencies-form/add-currencies-form.tsx
new file mode 100644
index 0000000000..13d52a0dc2
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/store/store-add-currencies/components/add-currencies-form/add-currencies-form.tsx
@@ -0,0 +1,338 @@
+import { Currency } from "@medusajs/medusa"
+import {
+ Badge,
+ Button,
+ Checkbox,
+ Hint,
+ StatusBadge,
+ Table,
+ Tooltip,
+ clx,
+} from "@medusajs/ui"
+import {
+ PaginationState,
+ RowSelectionState,
+ createColumnHelper,
+ flexRender,
+ getCoreRowModel,
+ useReactTable,
+} from "@tanstack/react-table"
+import {
+ adminCurrenciesKeys,
+ adminStoreKeys,
+ useAdminCustomPost,
+ useAdminCustomQuery,
+} from "medusa-react"
+import { useEffect, useMemo, useState } from "react"
+import { useTranslation } from "react-i18next"
+import * as zod from "zod"
+
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useForm } from "react-hook-form"
+import { OrderBy } from "../../../../../components/filtering/order-by"
+import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
+import {
+ RouteFocusModal,
+ useRouteModal,
+} from "../../../../../components/route-modal"
+import { useHandleTableScroll } from "../../../../../hooks/use-handle-table-scroll"
+import { useQueryParams } from "../../../../../hooks/use-query-params"
+import { StoreDTO } from "@medusajs/types"
+
+type AddCurrenciesFormProps = {
+ store: StoreDTO
+}
+
+const AddCurrenciesSchema = zod.object({
+ currencies: zod.array(zod.string()).min(1),
+})
+
+const PAGE_SIZE = 50
+
+export const AddCurrenciesForm = ({ store }: AddCurrenciesFormProps) => {
+ const { t } = useTranslation()
+ const { handleSuccess } = useRouteModal()
+
+ const form = useForm>({
+ defaultValues: {
+ currencies: [],
+ },
+ resolver: zodResolver(AddCurrenciesSchema),
+ })
+
+ const { setValue } = form
+
+ const [{ pageIndex, pageSize }, setPagination] = useState({
+ pageIndex: 0,
+ pageSize: PAGE_SIZE,
+ })
+
+ const pagination = useMemo(
+ () => ({
+ pageIndex,
+ pageSize,
+ }),
+ [pageIndex, pageSize]
+ )
+
+ const [rowSelection, setRowSelection] = useState({})
+
+ useEffect(() => {
+ const ids = Object.keys(rowSelection)
+ setValue("currencies", ids, {
+ shouldDirty: true,
+ shouldTouch: true,
+ })
+ }, [rowSelection, setValue])
+
+ const params = useQueryParams(["order"])
+ const filter = {
+ limit: PAGE_SIZE,
+ offset: pageIndex * PAGE_SIZE,
+ ...params,
+ }
+ // @ts-ignore
+ const { data, count, isError, error } = useAdminCustomQuery(
+ "/admin/currencies",
+ adminCurrenciesKeys.list(filter),
+ filter
+ )
+
+ const preSelectedRows = store.supported_currency_codes.map((c) => c)
+
+ const columns = useColumns()
+
+ const table = useReactTable({
+ data: data?.currencies ?? [],
+ columns,
+ pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
+ state: {
+ pagination,
+ rowSelection,
+ },
+ getRowId: (row) => row.code,
+ onPaginationChange: setPagination,
+ onRowSelectionChange: setRowSelection,
+ getCoreRowModel: getCoreRowModel(),
+ enableRowSelection: (row) => !preSelectedRows.includes(row.original.code),
+ manualPagination: true,
+ })
+
+ const { mutateAsync, isLoading: isMutating } = useAdminCustomPost(
+ `/admin/stores/${store.id}`,
+ adminStoreKeys.details()
+ )
+
+ const { handleScroll, isScrolled, tableContainerRef } = useHandleTableScroll()
+
+ const handleSubmit = form.handleSubmit(async (data) => {
+ const currencies = Array.from(
+ new Set([...data.currencies, ...preSelectedRows])
+ ) as string[]
+
+ await mutateAsync(
+ {
+ supported_currency_codes: currencies,
+ },
+ {
+ onSuccess: () => {
+ handleSuccess()
+ },
+ }
+ )
+ })
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+
+
+ )
+}
+
+const columnHelper = createColumnHelper()
+
+const useColumns = () => {
+ const { t } = useTranslation()
+
+ return useMemo(
+ () => [
+ columnHelper.display({
+ id: "select",
+ header: ({ table }) => {
+ return (
+
+ table.toggleAllPageRowsSelected(!!value)
+ }
+ />
+ )
+ },
+ cell: ({ row }) => {
+ const isPreSelected = !row.getCanSelect()
+ const isSelected = row.getIsSelected() || isPreSelected
+
+ const Component = (
+ row.toggleSelected(!!value)}
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ />
+ )
+
+ if (isPreSelected) {
+ return (
+
+ {Component}
+
+ )
+ }
+
+ return Component
+ },
+ }),
+ columnHelper.accessor("name", {
+ header: t("fields.name"),
+ cell: ({ getValue }) => getValue(),
+ }),
+ columnHelper.accessor("code", {
+ header: t("fields.code"),
+ cell: ({ getValue }) => (
+ {getValue().toUpperCase()}
+ ),
+ }),
+ columnHelper.accessor("includes_tax", {
+ header: t("fields.taxInclusivePricing"),
+ cell: ({ getValue }) => {
+ const value = getValue()
+
+ return (
+
+ {value ? t("general.enabled") : t("general.disabled")}
+
+ )
+ },
+ }),
+ ],
+ [t]
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-add-currencies/index.ts b/packages/admin-next/dashboard/src/v2-routes/store/store-add-currencies/index.ts
new file mode 100644
index 0000000000..8a88a62bd8
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/store/store-add-currencies/index.ts
@@ -0,0 +1 @@
+export { StoreAddCurrencies as Component } from "./store-add-currencies"
diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-add-currencies/store-add-currencies.tsx b/packages/admin-next/dashboard/src/v2-routes/store/store-add-currencies/store-add-currencies.tsx
new file mode 100644
index 0000000000..e9c5c1644b
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/store/store-add-currencies/store-add-currencies.tsx
@@ -0,0 +1,17 @@
+import { RouteFocusModal } from "../../../components/route-modal"
+import { AddCurrenciesForm } from "./components/add-currencies-form/add-currencies-form"
+import { useV2Store } from "../../../lib/api-v2"
+
+export const StoreAddCurrencies = () => {
+ const { store, isLoading, isError, error } = useV2Store({})
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+ {!isLoading && store && }
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-currency-section/store-currencies-section.tsx/index.ts b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-currency-section/store-currencies-section.tsx/index.ts
new file mode 100644
index 0000000000..9a84261a47
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-currency-section/store-currencies-section.tsx/index.ts
@@ -0,0 +1 @@
+export * from "./store-currency-section"
diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-currency-section/store-currencies-section.tsx/store-currency-section.tsx b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-currency-section/store-currencies-section.tsx/store-currency-section.tsx
new file mode 100644
index 0000000000..9d63c87c18
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-currency-section/store-currencies-section.tsx/store-currency-section.tsx
@@ -0,0 +1,315 @@
+import { Trash } from "@medusajs/icons"
+import { Currency } from "@medusajs/medusa"
+import {
+ Button,
+ Checkbox,
+ CommandBar,
+ Container,
+ Heading,
+ StatusBadge,
+ Table,
+ clx,
+ usePrompt,
+} from "@medusajs/ui"
+import {
+ RowSelectionState,
+ createColumnHelper,
+ flexRender,
+ getCoreRowModel,
+ getPaginationRowModel,
+ useReactTable,
+} from "@tanstack/react-table"
+import {
+ adminStoreKeys,
+ useAdminCustomPost,
+ useAdminCustomQuery,
+} from "medusa-react"
+import { useMemo, useState } from "react"
+import { useTranslation } from "react-i18next"
+import { Link } from "react-router-dom"
+import { ActionMenu } from "../../../../../../components/common/action-menu"
+import { LocalizedTablePagination } from "../../../../../../components/localization/localized-table-pagination"
+import { StoreDTO } from "@medusajs/types"
+
+type StoreCurrencySectionProps = {
+ store: StoreDTO
+}
+
+const PAGE_SIZE = 20
+
+export const StoreCurrencySection = ({ store }: StoreCurrencySectionProps) => {
+ const [rowSelection, setRowSelection] = useState({})
+ const { data } = useAdminCustomQuery(
+ `/admin/currencies?code[]=${store.supported_currency_codes.join(",")}`,
+ adminStoreKeys.details()
+ )
+
+ const columns = useColumns()
+
+ const table = useReactTable({
+ data: data?.currencies ?? [],
+ columns,
+ getCoreRowModel: getCoreRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ onRowSelectionChange: setRowSelection,
+ getRowId: (row) => row.code,
+ pageCount: Math.ceil(store.supported_currency_codes.length / PAGE_SIZE),
+ state: {
+ rowSelection,
+ },
+ meta: {
+ currencyCodes: store.supported_currency_codes,
+ storeId: store.id,
+ },
+ })
+
+ const { mutateAsync } = useAdminCustomPost(
+ `/admin/stores/${store.id}`,
+ adminStoreKeys.details()
+ )
+ const { t } = useTranslation()
+ const prompt = usePrompt()
+
+ const handleDeleteCurrencies = async () => {
+ const ids = Object.keys(rowSelection)
+
+ const result = await prompt({
+ title: t("general.areYouSure"),
+ description: t("store.removeCurrencyWarning", {
+ count: ids.length,
+ }),
+ confirmText: t("actions.remove"),
+ cancelText: t("actions.cancel"),
+ })
+
+ if (!result) {
+ return
+ }
+
+ await mutateAsync(
+ {
+ supported_currency_codes: store.supported_currency_codes.filter(
+ (c) => !ids.includes(c)
+ ),
+ },
+ {
+ onSuccess: () => {
+ setRowSelection({})
+ },
+ }
+ )
+ }
+
+ return (
+
+
+
{t("store.currencies")}
+
+
+
+
+
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => {
+ return (
+
+ {headerGroup.headers.map((header) => {
+ return (
+
+ {flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
+
+ )
+ })}
+
+ )
+ })}
+
+
+ {table.getRowModel().rows.map((row) => (
+
+ {row.getVisibleCells().map((cell) => (
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ ))}
+
+ ))}
+
+
+
+
+
+
+ {t("general.countSelected", {
+ count: Object.keys(rowSelection).length,
+ })}
+
+
+
+
+
+
+ )
+}
+
+const CurrencyActions = ({
+ storeId,
+ currency,
+ currencyCodes,
+}: {
+ storeId: string
+ currency: Currency
+ currencyCodes: string[]
+}) => {
+ const { mutateAsync } = useAdminCustomPost(
+ `/admin/stores/${storeId}`,
+ adminStoreKeys.details()
+ )
+
+ const { t } = useTranslation()
+ const prompt = usePrompt()
+
+ const handleRemove = async () => {
+ const result = await prompt({
+ title: t("general.areYouSure"),
+ description: t("store.removeCurrencyWarning", {
+ count: 1,
+ }),
+ verificationInstruction: t("general.typeToConfirm"),
+ verificationText: currency.name,
+ confirmText: t("actions.remove"),
+ cancelText: t("actions.cancel"),
+ })
+
+ if (!result) {
+ return
+ }
+
+ await mutateAsync({
+ supported_currency_codes: currencyCodes.filter(
+ (c) => c !== currency.code
+ ),
+ })
+ }
+
+ return (
+ ,
+ label: t("actions.remove"),
+ onClick: handleRemove,
+ },
+ ],
+ },
+ ]}
+ />
+ )
+}
+
+const columnHelper = createColumnHelper()
+
+const useColumns = () => {
+ const { t } = useTranslation()
+
+ return useMemo(
+ () => [
+ columnHelper.display({
+ id: "select",
+ header: ({ table }) => {
+ return (
+
+ table.toggleAllPageRowsSelected(!!value)
+ }
+ />
+ )
+ },
+ cell: ({ row }) => {
+ return (
+ row.toggleSelected(!!value)}
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ />
+ )
+ },
+ }),
+ columnHelper.accessor("code", {
+ header: t("fields.code"),
+ cell: ({ getValue }) => getValue().toUpperCase(),
+ }),
+ columnHelper.accessor("name", {
+ header: t("fields.name"),
+ cell: ({ getValue }) => getValue(),
+ }),
+ columnHelper.accessor("includes_tax", {
+ header: "Tax Inclusive Prices",
+ cell: ({ getValue }) => {
+ const value = getValue()
+ const text = value ? t("general.enabled") : t("general.disabled")
+
+ return (
+ {text}
+ )
+ },
+ }),
+ columnHelper.display({
+ id: "actions",
+ cell: ({ row, table }) => {
+ const { currencyCodes, storeId } = table.options.meta as {
+ currencyCodes: string[]
+ storeId: string
+ }
+
+ return (
+
+ )
+ },
+ }),
+ ],
+ [t]
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-general-section/index.ts b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-general-section/index.ts
new file mode 100644
index 0000000000..0ea68a992d
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-general-section/index.ts
@@ -0,0 +1 @@
+export * from "./store-general-section"
diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-general-section/store-general-section.tsx b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-general-section/store-general-section.tsx
new file mode 100644
index 0000000000..ccc4f022de
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-general-section/store-general-section.tsx
@@ -0,0 +1,114 @@
+import { Store } from "@medusajs/medusa"
+import { Badge, Button, Container, Copy, Heading, Text } from "@medusajs/ui"
+import { useTranslation } from "react-i18next"
+import { Link } from "react-router-dom"
+
+type StoreGeneralSectionProps = {
+ store: Store
+}
+
+export const StoreGeneralSection = ({ store }: StoreGeneralSectionProps) => {
+ const { t } = useTranslation()
+
+ return (
+
+
+
+ {t("store.domain")}
+
+ {t("store.manageYourStoresDetails")}
+
+
+
+
+
+
+
+
+ {t("fields.name")}
+
+
+ {store.name}
+
+
+
+
+ {t("store.defaultCurrency")}
+
+
+
+ {store.default_currency_code.toUpperCase()}
+
+
+ {store.default_currency.name}
+
+
+
+
+
+ {t("store.swapLinkTemplate")}
+
+ {store.swap_link_template ? (
+
+
+ {store.swap_link_template}
+
+
+
+ ) : (
+
+ -
+
+ )}
+
+
+
+ {t("store.paymentLinkTemplate")}
+
+ {store.payment_link_template ? (
+
+
+ {store.payment_link_template}
+
+
+
+ ) : (
+
+ -
+
+ )}
+
+
+
+ {t("store.inviteLinkTemplate")}
+
+ {store.invite_link_template ? (
+
+
+ {store.invite_link_template}
+
+
+
+ ) : (
+
+ -
+
+ )}
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-detail/index.ts b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/index.ts
new file mode 100644
index 0000000000..bc9d614088
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/index.ts
@@ -0,0 +1,2 @@
+export { storeLoader as loader } from "./loader"
+export { StoreDetail as Component } from "./store-detail"
diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-detail/loader.ts b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/loader.ts
new file mode 100644
index 0000000000..4060a8000c
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/loader.ts
@@ -0,0 +1,38 @@
+import { AdminExtendedStoresRes } from "@medusajs/medusa"
+import { Response } from "@medusajs/medusa-js"
+import { adminStoreKeys } from "medusa-react"
+import { redirect } from "react-router-dom"
+
+import { FetchQueryOptions } from "@tanstack/react-query"
+import { medusa, queryClient } from "../../../lib/medusa"
+
+const storeDetailQuery = () => ({
+ queryKey: adminStoreKeys.details(),
+ queryFn: async () => medusa.client.request("GET", "/admin/stores"),
+})
+
+const fetchQuery = async (
+ query: FetchQueryOptions>
+) => {
+ try {
+ const res = await queryClient.fetchQuery(query)
+ // TODO: Reconsider store retrieval
+ return res
+ } catch (error) {
+ const err = error ? JSON.parse(JSON.stringify(error)) : null
+
+ if ((err as Error & { status: number })?.status === 401) {
+ redirect("/login", 401)
+ }
+ }
+}
+
+export const storeLoader = async () => {
+ const query = storeDetailQuery()
+
+ return (
+ queryClient.getQueryData>(
+ query.queryKey
+ ) ?? (await fetchQuery(query))
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-detail/store-detail.tsx b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/store-detail.tsx
new file mode 100644
index 0000000000..1b9e0a2b65
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/store-detail.tsx
@@ -0,0 +1,36 @@
+import { Outlet, useLoaderData } from "react-router-dom"
+
+import { JsonViewSection } from "../../../components/common/json-view-section/json-view-section.tsx"
+import { StoreCurrencySection } from "./components/store-currency-section/store-currencies-section.tsx/index.ts"
+import { StoreGeneralSection } from "./components/store-general-section/index.ts"
+import { storeLoader } from "./loader.ts"
+import { useV2Store } from "../../../lib/api-v2/index.ts"
+
+export const StoreDetail = () => {
+ const initialData = useLoaderData() as Awaited>
+
+ const { store, isLoading, isError, error } = useV2Store({
+ initialData: initialData,
+ })
+
+ if (isLoading) {
+ return Loading...
+ }
+
+ if (isError || !store) {
+ if (error) {
+ throw error
+ }
+
+ return {JSON.stringify(error, null, 2)}
+ }
+
+ return (
+
+
+
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-edit/components/edit-store-form/edit-store-form.tsx b/packages/admin-next/dashboard/src/v2-routes/store/store-edit/components/edit-store-form/edit-store-form.tsx
new file mode 100644
index 0000000000..e9f8257149
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/store/store-edit/components/edit-store-form/edit-store-form.tsx
@@ -0,0 +1,149 @@
+import { zodResolver } from "@hookform/resolvers/zod"
+import type { Store } from "@medusajs/medusa"
+import { Button, Input } from "@medusajs/ui"
+import { adminStoreKeys, useAdminCustomPost } from "medusa-react"
+import { useForm } from "react-hook-form"
+import { useTranslation } from "react-i18next"
+import * as zod from "zod"
+import { Form } from "../../../../../components/common/form"
+import {
+ RouteDrawer,
+ useRouteModal,
+} from "../../../../../components/route-modal"
+
+type EditStoreFormProps = {
+ store: Store
+}
+
+const EditStoreSchema = zod.object({
+ name: zod.string().optional(),
+ swap_link_template: zod.union([zod.literal(""), zod.string().trim().url()]),
+ payment_link_template: zod.union([
+ zod.literal(""),
+ zod.string().trim().url(),
+ ]),
+ invite_link_template: zod.union([zod.literal(""), zod.string().trim().url()]),
+})
+
+export const EditStoreForm = ({ store }: EditStoreFormProps) => {
+ const { t } = useTranslation()
+ const { handleSuccess } = useRouteModal()
+
+ const form = useForm>({
+ defaultValues: {
+ name: store.name,
+ swap_link_template: store.swap_link_template ?? "",
+ payment_link_template: store.payment_link_template ?? "",
+ invite_link_template: store.invite_link_template ?? "",
+ },
+ resolver: zodResolver(EditStoreSchema),
+ })
+
+ const { mutateAsync, isLoading } = useAdminCustomPost(
+ `/admin/stores/${store.id}`,
+ adminStoreKeys.details()
+ )
+
+ const handleSubmit = form.handleSubmit(async (values) => {
+ mutateAsync(
+ {
+ name: values.name,
+ // invite_link_template: values.invite_link_template || undefined,
+ // swap_link_template: values.swap_link_template || undefined,
+ // payment_link_template: values.payment_link_template || undefined,
+ },
+ {
+ onSuccess: () => {
+ handleSuccess()
+ },
+ }
+ )
+ })
+
+ return (
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-edit/index.ts b/packages/admin-next/dashboard/src/v2-routes/store/store-edit/index.ts
new file mode 100644
index 0000000000..16ec90284f
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/store/store-edit/index.ts
@@ -0,0 +1 @@
+export { StoreEdit as Component } from "./store-edit"
diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-edit/store-edit.tsx b/packages/admin-next/dashboard/src/v2-routes/store/store-edit/store-edit.tsx
new file mode 100644
index 0000000000..fe4d8daa74
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/store/store-edit/store-edit.tsx
@@ -0,0 +1,28 @@
+import { Heading } from "@medusajs/ui"
+import { useTranslation } from "react-i18next"
+import { json } from "react-router-dom"
+import { RouteDrawer } from "../../../components/route-modal"
+import { EditStoreForm } from "./components/edit-store-form/edit-store-form"
+import { useV2Store } from "../../../lib/api-v2"
+
+export const StoreEdit = () => {
+ const { t } = useTranslation()
+ const { store, isLoading, isError, error } = useV2Store({})
+
+ if (isError) {
+ throw error
+ }
+
+ if (!store && !isLoading) {
+ throw json("An unknown error has occured", 500)
+ }
+
+ return (
+
+
+ {t("store.editStore")}
+
+ {store && }
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/vite-env.d.ts b/packages/admin-next/dashboard/src/vite-env.d.ts
index 0351443b82..04d24530dd 100644
--- a/packages/admin-next/dashboard/src/vite-env.d.ts
+++ b/packages/admin-next/dashboard/src/vite-env.d.ts
@@ -2,7 +2,7 @@
interface ImportMetaEnv {
readonly MEDUSA_ADMIN_BACKEND_URL: string
- readonly MEDUSA_V2: boolean
+ readonly VITE_MEDUSA_V2: boolean
}
interface ImportMeta {
diff --git a/packages/auth/src/providers/email-password.ts b/packages/auth/src/providers/email-password.ts
index fc239438a7..39a9c95a7b 100644
--- a/packages/auth/src/providers/email-password.ts
+++ b/packages/auth/src/providers/email-password.ts
@@ -1,9 +1,9 @@
+import { AuthenticationInput, AuthenticationResponse } from "@medusajs/types"
import {
AbstractAuthModuleProvider,
MedusaError,
isString,
} from "@medusajs/utils"
-import { AuthenticationInput, AuthenticationResponse } from "@medusajs/types"
import { AuthUserService } from "@services"
import Scrypt from "scrypt-kdf"
diff --git a/packages/core-flows/src/defaults/index.ts b/packages/core-flows/src/defaults/index.ts
new file mode 100644
index 0000000000..68de82c9f9
--- /dev/null
+++ b/packages/core-flows/src/defaults/index.ts
@@ -0,0 +1,2 @@
+export * from "./steps"
+export * from "./workflows"
diff --git a/packages/core-flows/src/defaults/steps/create-default-store.ts b/packages/core-flows/src/defaults/steps/create-default-store.ts
new file mode 100644
index 0000000000..5c8c95d1b7
--- /dev/null
+++ b/packages/core-flows/src/defaults/steps/create-default-store.ts
@@ -0,0 +1,49 @@
+import { ModuleRegistrationName } from "@medusajs/modules-sdk"
+import { CreateStoreDTO, IStoreModuleService } from "@medusajs/types"
+import { StepResponse, createStep } from "@medusajs/workflows-sdk"
+import { createStoresWorkflow } from "../../store"
+
+type CreateDefaultStoreStepInput = {
+ store: CreateStoreDTO
+}
+
+export const createDefaultStoreStepId = "create-default-store"
+export const createDefaultStoreStep = createStep(
+ createDefaultStoreStepId,
+ async (data: CreateDefaultStoreStepInput, { container }) => {
+ const storeService = container.resolve(ModuleRegistrationName.STORE)
+
+ let shouldDelete = false
+ let [store] = await storeService.list({}, { take: 1 })
+
+ if (!store) {
+ store = await createStoresWorkflow(container).run({
+ input: {
+ stores: [
+ {
+ // TODO: Revisit for a more sophisticated approach
+ ...data.store,
+ supported_currency_codes: ["usd"],
+ default_currency_code: "usd",
+ },
+ ],
+ },
+ })
+
+ shouldDelete = true
+ }
+
+ return new StepResponse(store, { storeId: store.id, shouldDelete })
+ },
+ async (data, { container }) => {
+ if (!data || !data.shouldDelete) {
+ return
+ }
+
+ const service = container.resolve(
+ ModuleRegistrationName.STORE
+ )
+
+ await service.delete(data.storeId)
+ }
+)
diff --git a/packages/core-flows/src/defaults/steps/index.ts b/packages/core-flows/src/defaults/steps/index.ts
new file mode 100644
index 0000000000..aea300000d
--- /dev/null
+++ b/packages/core-flows/src/defaults/steps/index.ts
@@ -0,0 +1 @@
+export * from "./create-default-store"
diff --git a/packages/core-flows/src/defaults/workflows/create-defaults.ts b/packages/core-flows/src/defaults/workflows/create-defaults.ts
new file mode 100644
index 0000000000..847a79996d
--- /dev/null
+++ b/packages/core-flows/src/defaults/workflows/create-defaults.ts
@@ -0,0 +1,23 @@
+import { createWorkflow } from "@medusajs/workflows-sdk"
+import { createDefaultSalesChannelStep } from "../../sales-channel"
+import { createDefaultStoreStep } from "../steps/create-default-store"
+
+export const createDefaultsWorkflowID = "create-defaults"
+export const createDefaultsWorkflow = createWorkflow(
+ createDefaultsWorkflowID,
+ (input) => {
+ const salesChannel = createDefaultSalesChannelStep({
+ data: {
+ name: "Default Sales Channel",
+ description: "Created by Medusa",
+ },
+ })
+ const store = createDefaultStoreStep({
+ store: {
+ default_sales_channel_id: salesChannel.id,
+ },
+ })
+
+ return store
+ }
+)
diff --git a/packages/core-flows/src/defaults/workflows/index.ts b/packages/core-flows/src/defaults/workflows/index.ts
new file mode 100644
index 0000000000..9f965e552e
--- /dev/null
+++ b/packages/core-flows/src/defaults/workflows/index.ts
@@ -0,0 +1 @@
+export * from "./create-defaults"
diff --git a/packages/core-flows/src/index.ts b/packages/core-flows/src/index.ts
index 12d1a64135..00f215b61b 100644
--- a/packages/core-flows/src/index.ts
+++ b/packages/core-flows/src/index.ts
@@ -2,6 +2,7 @@ export * from "./api-key"
export * from "./auth"
export * from "./customer"
export * from "./customer-group"
+export * from "./defaults"
export * from "./definition"
export * from "./definitions"
export * from "./fulfillment"
diff --git a/packages/core-flows/src/sales-channel/index.ts b/packages/core-flows/src/sales-channel/index.ts
index 68de82c9f9..e58562ad24 100644
--- a/packages/core-flows/src/sales-channel/index.ts
+++ b/packages/core-flows/src/sales-channel/index.ts
@@ -1,2 +1,3 @@
export * from "./steps"
export * from "./workflows"
+
diff --git a/packages/core-flows/src/sales-channel/steps/create-default-sales-channel.ts b/packages/core-flows/src/sales-channel/steps/create-default-sales-channel.ts
new file mode 100644
index 0000000000..d28b210fad
--- /dev/null
+++ b/packages/core-flows/src/sales-channel/steps/create-default-sales-channel.ts
@@ -0,0 +1,42 @@
+import { ModuleRegistrationName } from "@medusajs/modules-sdk"
+import {
+ CreateSalesChannelDTO,
+ ISalesChannelModuleService,
+} from "@medusajs/types"
+import { StepResponse, createStep } from "@medusajs/workflows-sdk"
+
+interface StepInput {
+ data: CreateSalesChannelDTO
+}
+
+export const createDefaultSalesChannelStepId = "create-default-sales-channel"
+export const createDefaultSalesChannelStep = createStep(
+ createDefaultSalesChannelStepId,
+ async (input: StepInput, { container }) => {
+ const salesChannelService = container.resolve(
+ ModuleRegistrationName.SALES_CHANNEL
+ )
+
+ let shouldDelete = false
+ let [salesChannel] = await salesChannelService.list({}, { take: 1 })
+
+ if (!salesChannel) {
+ salesChannel = await salesChannelService.create(input.data)
+
+ shouldDelete = true
+ }
+
+ return new StepResponse(salesChannel, { id: salesChannel.id, shouldDelete })
+ },
+ async (data, { container }) => {
+ if (!data?.id || !data.shouldDelete) {
+ return
+ }
+
+ const service = container.resolve(
+ ModuleRegistrationName.SALES_CHANNEL
+ )
+
+ await service.delete(data.id)
+ }
+)
diff --git a/packages/core-flows/src/sales-channel/steps/index.ts b/packages/core-flows/src/sales-channel/steps/index.ts
index cd51b9588a..662d94bce6 100644
--- a/packages/core-flows/src/sales-channel/steps/index.ts
+++ b/packages/core-flows/src/sales-channel/steps/index.ts
@@ -1,4 +1,5 @@
export * from "./associate-products-with-channels"
+export * from "./create-default-sales-channel"
export * from "./create-sales-channels"
export * from "./delete-sales-channels"
export * from "./update-sales-channels"
diff --git a/packages/currency/src/joiner-config.ts b/packages/currency/src/joiner-config.ts
index dc807cb8ae..fe1da1c054 100644
--- a/packages/currency/src/joiner-config.ts
+++ b/packages/currency/src/joiner-config.ts
@@ -3,7 +3,9 @@ import { ModuleJoinerConfig } from "@medusajs/types"
import { MapToConfig } from "@medusajs/utils"
import Currency from "./models/currency"
-export const LinkableKeys: Record = {}
+export const LinkableKeys: Record = {
+ code: Currency.name,
+}
const entityLinkableKeysMap: MapToConfig = {}
Object.entries(LinkableKeys).forEach(([key, value]) => {
diff --git a/packages/link-modules/src/definitions/index.ts b/packages/link-modules/src/definitions/index.ts
index 39d7f87935..f7246f0717 100644
--- a/packages/link-modules/src/definitions/index.ts
+++ b/packages/link-modules/src/definitions/index.ts
@@ -14,4 +14,5 @@ export * from "./publishable-api-key-sales-channel"
export * from "./region-payment-provider"
export * from "./sales-channel-location"
export * from "./shipping-option-price-set"
+export * from "./store-default-currency"
diff --git a/packages/link-modules/src/definitions/store-default-currency.ts b/packages/link-modules/src/definitions/store-default-currency.ts
new file mode 100644
index 0000000000..b2d3828256
--- /dev/null
+++ b/packages/link-modules/src/definitions/store-default-currency.ts
@@ -0,0 +1,18 @@
+import { Modules } from "@medusajs/modules-sdk"
+import { ModuleJoinerConfig } from "@medusajs/types"
+
+export const StoreDefaultCurrency: ModuleJoinerConfig = {
+ isLink: true,
+ isReadOnlyLink: true,
+ extends: [
+ {
+ serviceName: Modules.STORE,
+ relationship: {
+ serviceName: Modules.CURRENCY,
+ primaryKey: "code",
+ foreignKey: "default_currency_code",
+ alias: "default_currency",
+ },
+ },
+ ],
+}
diff --git a/packages/medusa/src/api-v2/admin/stores/query-config.ts b/packages/medusa/src/api-v2/admin/stores/query-config.ts
index dc879e64c5..07e49acdbf 100644
--- a/packages/medusa/src/api-v2/admin/stores/query-config.ts
+++ b/packages/medusa/src/api-v2/admin/stores/query-config.ts
@@ -1,10 +1,11 @@
-export const defaultAdminStoreRelations = []
-export const allowedAdminStoreRelations = []
export const defaultAdminStoreFields = [
"id",
"name",
"supported_currency_codes",
"default_currency_code",
+ "default_currency.name",
+ "default_currency.symbol",
+ "default_currency.symbol_native",
"default_sales_channel_id",
"default_region_id",
"default_location_id",
@@ -14,9 +15,7 @@ export const defaultAdminStoreFields = [
]
export const retrieveTransformQueryConfig = {
- defaultFields: defaultAdminStoreFields,
- defaultRelations: defaultAdminStoreRelations,
- allowedRelations: allowedAdminStoreRelations,
+ defaults: defaultAdminStoreFields,
isList: false,
}
diff --git a/packages/medusa/src/api-v2/admin/stores/route.ts b/packages/medusa/src/api-v2/admin/stores/route.ts
index 5f6750481b..ca70ca7347 100644
--- a/packages/medusa/src/api-v2/admin/stores/route.ts
+++ b/packages/medusa/src/api-v2/admin/stores/route.ts
@@ -1,6 +1,6 @@
import {
ContainerRegistrationKeys,
- remoteQueryObjectFromString
+ remoteQueryObjectFromString,
} from "@medusajs/utils"
import { MedusaRequest, MedusaResponse } from "../../../types/routing"
import { defaultAdminStoreFields } from "./query-config"
diff --git a/packages/medusa/src/commands/user.js b/packages/medusa/src/commands/user.js
index 9870cfd684..a5039da207 100644
--- a/packages/medusa/src/commands/user.js
+++ b/packages/medusa/src/commands/user.js
@@ -6,6 +6,31 @@ import { track } from "medusa-telemetry"
import loaders from "../loaders"
import Logger from "../loaders/logger"
+import { ModuleRegistrationName } from "@medusajs/modules-sdk"
+import featureFlagLoader from "../loaders/feature-flags"
+import configModuleLoader from "../loaders/config"
+import { MedusaV2Flag } from "@medusajs/utils"
+
+// TEMP: Only supporting emailpass
+const createV2User = async ({ email, password }, { container }) => {
+ const authService = container.resolve(ModuleRegistrationName.AUTH)
+ const userService = container.resolve(ModuleRegistrationName.USER)
+ const user = await userService.create({ email })
+ const { authUser } = await authService.authenticate("emailpass", {
+ body: {
+ email,
+ password,
+ },
+ authScope: "admin",
+ })
+
+ await authService.update({
+ id: authUser.id,
+ app_metadata: {
+ user_id: user.id,
+ },
+ })
+}
export default async function ({
directory,
@@ -33,8 +58,15 @@ export default async function ({
Invite token: ${invite[0].token}
Open the invite in Medusa Admin at: [your-admin-url]/invite?token=${invite[0].token}`)
} else {
- const userService = container.resolve("userService")
- await userService.create({ id, email }, password)
+ const configModule = configModuleLoader(directory)
+ const featureFlagRouter = featureFlagLoader(configModule)
+
+ if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
+ await createV2User({ email, password }, { container })
+ } else {
+ const userService = container.resolve("userService")
+ await userService.create({ id, email }, password)
+ }
}
} catch (err) {
console.error(err)
diff --git a/packages/medusa/src/loaders/helpers/routing/index.ts b/packages/medusa/src/loaders/helpers/routing/index.ts
index dee0a94c18..bf95351030 100644
--- a/packages/medusa/src/loaders/helpers/routing/index.ts
+++ b/packages/medusa/src/loaders/helpers/routing/index.ts
@@ -11,6 +11,7 @@ import {
requireCustomerAuthentication,
} from "../../../api/middlewares"
import { ConfigModule } from "../../../types/global"
+import { MedusaRequest, MedusaResponse } from "../../../types/routing"
import logger from "../../logger"
import {
AsyncRouteHandler,
@@ -19,12 +20,11 @@ import {
MiddlewareRoute,
MiddlewareVerb,
MiddlewaresConfig,
+ ParserConfigArgs,
RouteConfig,
RouteDescriptor,
RouteVerb,
- ParserConfigArgs,
} from "./types"
-import { MedusaRequest, MedusaResponse } from "../../../types/routing"
const log = ({
activityId,
diff --git a/packages/medusa/src/loaders/index.ts b/packages/medusa/src/loaders/index.ts
index 3741264d37..fa2b9d798f 100644
--- a/packages/medusa/src/loaders/index.ts
+++ b/packages/medusa/src/loaders/index.ts
@@ -1,12 +1,13 @@
+import { createDefaultsWorkflow } from "@medusajs/core-flows"
import {
InternalModuleDeclaration,
- ModulesDefinition,
+ ModulesDefinition
} from "@medusajs/modules-sdk"
import { MODULE_RESOURCE_TYPE } from "@medusajs/types"
import {
ContainerRegistrationKeys,
- isString,
MedusaV2Flag,
+ isString,
} from "@medusajs/utils"
import { asValue } from "awilix"
import { Express, NextFunction, Request, Response } from "express"
@@ -23,6 +24,7 @@ import databaseLoader, { dataSource } from "./database"
import defaultsLoader from "./defaults"
import expressLoader from "./express"
import featureFlagsLoader from "./feature-flags"
+import medusaProjectApisLoader from "./load-medusa-project-apis"
import Logger from "./logger"
import loadMedusaApp, { mergeDefaultModules } from "./medusa-app"
import modelsLoader from "./models"
@@ -35,7 +37,6 @@ import searchIndexLoader from "./search-index"
import servicesLoader from "./services"
import strategiesLoader from "./strategies"
import subscribersLoader from "./subscribers"
-import medusaProjectApisLoader from "./load-medusa-project-apis"
type Options = {
directory: string
@@ -130,7 +131,7 @@ async function loadMedusaV2({
featureFlagRouter,
})
- medusaProjectApisLoader({
+ await medusaProjectApisLoader({
rootDirectory,
container,
app: expressApp,
@@ -138,6 +139,8 @@ async function loadMedusaV2({
activityId: "medusa-project-apis",
})
+ await createDefaultsWorkflow(container).run()
+
return {
container,
app: expressApp,