@hartl-services/medusa-storefront 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/browser/actions.d.ts +6 -0
- package/dist/browser/cart-mutations.d.ts +2 -1
- package/dist/browser/cart-mutations.js +2 -2
- package/dist/browser/index.d.ts +5 -1
- package/dist/browser/query-keys.d.ts +1 -0
- package/dist/browser/query-keys.js +2 -0
- package/dist/browser/query-options.d.ts +4 -0
- package/dist/browser/query-options.js +6 -0
- package/dist/core/catalog/category-filter.d.ts +16 -0
- package/dist/core/catalog/category-filter.js +33 -0
- package/dist/core/catalog/partner-only.d.ts +18 -0
- package/dist/core/catalog/partner-only.js +31 -0
- package/dist/core/form-values.d.ts +96 -4
- package/dist/core/form-values.js +131 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/report-error.d.ts +1 -1
- package/dist/core/texts-de.d.ts +4 -0
- package/dist/core/texts-de.js +4 -0
- package/dist/react/use-account.d.ts +10 -0
- package/dist/react/use-account.js +10 -0
- package/dist/react/use-auth.d.ts +16 -1
- package/dist/react/use-auth.js +67 -0
- package/dist/react/use-cart.d.ts +26 -1
- package/dist/react/use-cart.js +50 -3
- package/dist/react/use-checkout-address-form.d.ts +12 -12
- package/dist/react/use-checkout.d.ts +8 -8
- package/dist/registry/account.json +2 -2
- package/dist/registry/affiliate.json +2 -2
- package/dist/registry/cart.json +1 -1
- package/dist/registry/checkout.json +1 -1
- package/dist/registry/form.json +1 -1
- package/dist/registry/registry.json +9 -9
- package/dist/registry/setup.json +3 -3
- package/dist/server/affiliate.d.ts +4 -1
- package/dist/server/affiliate.js +29 -0
- package/dist/server/catalog.d.ts +17 -3
- package/dist/server/catalog.js +54 -6
- package/dist/server/customer.d.ts +39 -2
- package/dist/server/customer.js +174 -4
- package/dist/server/index.d.ts +25 -1
- package/dist/server/index.js +4 -3
- package/package.json +2 -2
package/dist/server/catalog.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
import { cache } from "react";
|
|
2
|
+
import { collectPartnerOnlyCategoryIds, isPartnerOnlyProduct, withoutPartnerOnlyCategories } from "../core/catalog/partner-only.js";
|
|
1
3
|
import { CATEGORY_FIELDS, COLLECTION_FIELDS, PRODUCT_FIELDS } from "../core/fields.js";
|
|
2
4
|
const catalogFetchLimit = 100;
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Medusa's half of a catalog: products priced for a country, categories and
|
|
7
|
+
* collections. Partner-only products and categories (see
|
|
8
|
+
* `isPartnerOnlyProduct`) are left out unless `isActivePartner` says the
|
|
9
|
+
* visitor is an active partner; the backend refuses them at checkout anyway.
|
|
10
|
+
*/
|
|
11
|
+
export function createCatalog(context, region, isActivePartner = async () => false) {
|
|
5
12
|
async function getPricingContext(countryCode) {
|
|
6
13
|
const countryRegion = await region.getRegionForCountry(countryCode);
|
|
7
14
|
if (!countryRegion) {
|
|
@@ -13,8 +20,31 @@ export function createCatalog(context, region) {
|
|
|
13
20
|
region_id: countryRegion.id,
|
|
14
21
|
};
|
|
15
22
|
}
|
|
23
|
+
// Every top-level category with all descendants, once per request.
|
|
24
|
+
const loadCategoryTrees = cache(async () => {
|
|
25
|
+
const sdk = context.createMedusaClient();
|
|
26
|
+
const { product_categories: productCategories } = await sdk.store.category.list({
|
|
27
|
+
fields: "id,name,handle,description,metadata,rank,*category_children",
|
|
28
|
+
include_descendants_tree: true,
|
|
29
|
+
limit: catalogFetchLimit,
|
|
30
|
+
parent_category_id: "null",
|
|
31
|
+
});
|
|
32
|
+
return productCategories;
|
|
33
|
+
});
|
|
34
|
+
/**
|
|
35
|
+
* Leaves out partner-only products for everyone but an active partner. The
|
|
36
|
+
* partner check (a backend request) only runs when a partner-only product
|
|
37
|
+
* is among them, so a shop without any pays nothing for it; the category
|
|
38
|
+
* trees are only read when a product has categories.
|
|
39
|
+
*/
|
|
40
|
+
async function visibleProducts(products) {
|
|
41
|
+
const hasCategories = products.some((product) => (product.categories?.length ?? 0) > 0);
|
|
42
|
+
const partnerOnlyCategoryIds = hasCategories ? collectPartnerOnlyCategoryIds(await loadCategoryTrees()) : new Set();
|
|
43
|
+
const visible = products.filter((product) => !isPartnerOnlyProduct(product, partnerOnlyCategoryIds));
|
|
44
|
+
return visible.length === products.length || !(await isActivePartner()) ? visible : products;
|
|
45
|
+
}
|
|
16
46
|
return {
|
|
17
|
-
async listCommerceProducts({ categoryId, collectionId, countryCode, handles }) {
|
|
47
|
+
async listCommerceProducts({ categoryId, collectionId, countryCode, handles, q }) {
|
|
18
48
|
const pricingContext = await getPricingContext(countryCode);
|
|
19
49
|
if (!pricingContext) {
|
|
20
50
|
return null;
|
|
@@ -25,10 +55,11 @@ export function createCatalog(context, region) {
|
|
|
25
55
|
...(categoryId ? { category_id: [categoryId] } : {}),
|
|
26
56
|
...(collectionId ? { collection_id: [collectionId] } : {}),
|
|
27
57
|
...(handles?.length ? { handle: handles } : {}),
|
|
58
|
+
...(q?.trim() ? { q: q.trim() } : {}),
|
|
28
59
|
fields: PRODUCT_FIELDS,
|
|
29
60
|
limit: catalogFetchLimit,
|
|
30
61
|
});
|
|
31
|
-
return products;
|
|
62
|
+
return await visibleProducts(products);
|
|
32
63
|
},
|
|
33
64
|
async commerceProductByHandle({ countryCode, handle }) {
|
|
34
65
|
const pricingContext = await getPricingContext(countryCode);
|
|
@@ -42,7 +73,20 @@ export function createCatalog(context, region) {
|
|
|
42
73
|
handle,
|
|
43
74
|
limit: 1,
|
|
44
75
|
});
|
|
45
|
-
return products[0] ?? null;
|
|
76
|
+
return (await visibleProducts(products))[0] ?? null;
|
|
77
|
+
},
|
|
78
|
+
/**
|
|
79
|
+
* The top-level categories with all descendants (e.g. "Anwendungsbereiche"
|
|
80
|
+
* and "Inhaltsstoffe" with their entries) - the filter trees of a product
|
|
81
|
+
* list; filter with `filterProductsByCategories` from /core. Partner-only
|
|
82
|
+
* categories only for an active partner.
|
|
83
|
+
*/
|
|
84
|
+
async listCategoryTrees() {
|
|
85
|
+
const trees = await loadCategoryTrees();
|
|
86
|
+
if (collectPartnerOnlyCategoryIds(trees).size === 0) {
|
|
87
|
+
return trees;
|
|
88
|
+
}
|
|
89
|
+
return (await isActivePartner()) ? trees : withoutPartnerOnlyCategories(trees);
|
|
46
90
|
},
|
|
47
91
|
async categoryByPath(categoryPath) {
|
|
48
92
|
const sdk = context.createMedusaClient();
|
|
@@ -51,7 +95,11 @@ export function createCatalog(context, region) {
|
|
|
51
95
|
handle: categoryPath.join("/"),
|
|
52
96
|
limit: 1,
|
|
53
97
|
});
|
|
54
|
-
|
|
98
|
+
const category = productCategories[0] ?? null;
|
|
99
|
+
if (!category || !collectPartnerOnlyCategoryIds(await loadCategoryTrees()).has(category.id)) {
|
|
100
|
+
return category;
|
|
101
|
+
}
|
|
102
|
+
return (await isActivePartner()) ? category : null;
|
|
55
103
|
},
|
|
56
104
|
async collectionByHandle(handle) {
|
|
57
105
|
const sdk = context.createMedusaClient();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LoginWithCodeRequest, RequestLoginCodeRequest } from "@hartl-services/medusa-contracts/types/customer-auth-store";
|
|
2
|
-
import type { CheckoutAddressValues, CustomerAddressValues, CustomerAuthValues, CustomerProfileValues, VerificationResendValues } from "../core/form-values.js";
|
|
2
|
+
import type { CheckoutAddressValues, CustomerAddressValues, CustomerAuthValues, CustomerProfileValues, CustomerRegistrationProfileValues, PartnerRegistrationProfileValues, VerificationResendValues } from "../core/form-values.js";
|
|
3
3
|
import type { ServerContext } from "./context.js";
|
|
4
|
+
import type { ServerPluginFeatures } from "./plugin-features.js";
|
|
4
5
|
import type { RunMedusaAction } from "./run-medusa-action.js";
|
|
5
6
|
export interface UpdateCustomerAddressInput {
|
|
6
7
|
addressId: string;
|
|
@@ -10,7 +11,7 @@ export interface UpdateCustomerAddressInput {
|
|
|
10
11
|
metadata?: Record<string, unknown>;
|
|
11
12
|
};
|
|
12
13
|
}
|
|
13
|
-
export declare function createCustomer(context: ServerContext, runMedusaAction: RunMedusaAction): {
|
|
14
|
+
export declare function createCustomer(context: ServerContext, runMedusaAction: RunMedusaAction, pluginFeatures: ServerPluginFeatures): {
|
|
14
15
|
retrieveCustomer(): Promise<import("@medusajs/types").StoreCustomerResponse | null>;
|
|
15
16
|
loginCustomer(data: CustomerAuthValues): Promise<import("../core/action-result.js").MedusaActionResult<{
|
|
16
17
|
state: "success";
|
|
@@ -31,6 +32,42 @@ export declare function createCustomer(context: ServerContext, runMedusaAction:
|
|
|
31
32
|
state: "success";
|
|
32
33
|
sessionToken: string;
|
|
33
34
|
}>>;
|
|
35
|
+
/**
|
|
36
|
+
* Registration with name, phone and an optional address (the default
|
|
37
|
+
* address). The account counts as registered even if only the address
|
|
38
|
+
* fails (`addressSaved: false`, to add it in the account later) - a retry
|
|
39
|
+
* would otherwise end in "e-mail exists". While the e-mail is unconfirmed
|
|
40
|
+
* the address waits in the customer's metadata until the first login.
|
|
41
|
+
*/
|
|
42
|
+
registerCustomerWithProfile(data: CustomerRegistrationProfileValues): Promise<import("../core/action-result.js").MedusaActionResult<{
|
|
43
|
+
state: "verification_required";
|
|
44
|
+
addressId?: undefined;
|
|
45
|
+
addressSaved?: undefined;
|
|
46
|
+
sessionToken?: undefined;
|
|
47
|
+
} | {
|
|
48
|
+
addressSaved: boolean;
|
|
49
|
+
sessionToken: string;
|
|
50
|
+
state: "success";
|
|
51
|
+
}>>;
|
|
52
|
+
/**
|
|
53
|
+
* Partner (e.g. therapist) registration: the account with its company
|
|
54
|
+
* address as default address, then the partner application from that
|
|
55
|
+
* address. Like the plain registration it counts as registered even if a
|
|
56
|
+
* later step fails (`addressSaved` / `applicationSubmitted` false). While
|
|
57
|
+
* the e-mail is unconfirmed, address and application wait until the
|
|
58
|
+
* first login. Refused up front while the partner portal is off.
|
|
59
|
+
*/
|
|
60
|
+
registerPartnerWithProfile(data: PartnerRegistrationProfileValues): Promise<import("../core/action-result.js").MedusaActionResult<{
|
|
61
|
+
state: "verification_required";
|
|
62
|
+
addressId?: undefined;
|
|
63
|
+
addressSaved?: undefined;
|
|
64
|
+
sessionToken?: undefined;
|
|
65
|
+
} | {
|
|
66
|
+
addressSaved: boolean;
|
|
67
|
+
applicationSubmitted: boolean;
|
|
68
|
+
sessionToken: string;
|
|
69
|
+
state: "success";
|
|
70
|
+
}>>;
|
|
34
71
|
requestLoginCode(data: RequestLoginCodeRequest): Promise<import("../core/action-result.js").MedusaActionResult<void>>;
|
|
35
72
|
loginWithCode(data: LoginWithCodeRequest): Promise<import("../core/action-result.js").MedusaActionResult<{
|
|
36
73
|
state: "success";
|
package/dist/server/customer.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { customerAuthStorePaths } from "@hartl-services/medusa-contracts/paths/customer-auth";
|
|
2
2
|
import { EMAIL_OTP_AUTH_PROVIDER, LoginWithCodeSchema, RequestLoginCodeSchema, normalizeLoginEmail } from "@hartl-services/medusa-contracts/schemas/customer-auth";
|
|
3
3
|
import { customerVerificationStorePaths } from "@hartl-services/medusa-contracts/paths/customer-verification";
|
|
4
|
+
import { affiliateStorePaths } from "@hartl-services/medusa-contracts/paths/affiliate";
|
|
5
|
+
import { TAX_COMPLIANCE_FEATURES, TAX_COMPLIANCE_PLUGIN_KEY, taxCompliancePaths } from "@hartl-services/medusa-contracts/paths/tax-compliance";
|
|
4
6
|
import { vatNumberMetadataPatch } from "../core/addresses.js";
|
|
5
7
|
import { completeCustomerSignIn as completeCustomerSignInWith, loginWithEmailpass } from "../core/auth/customer-sign-in.js";
|
|
6
8
|
import { toCustomerAddressesFromCheckout } from "../core/checkout/checkout-address-draft.js";
|
|
7
9
|
import { StorefrontError } from "../core/errors.js";
|
|
8
10
|
import { CART_OWNER_FIELDS, CUSTOMER_FIELDS } from "../core/fields.js";
|
|
9
|
-
import { createCheckoutAddressSchema, createVerificationResendSchema, customerAddressSchema, customerAuthSchema, customerProfileSchema } from "../core/form-values.js";
|
|
11
|
+
import { createCheckoutAddressSchema, createVerificationResendSchema, customerAddressSchema, customerAuthSchema, customerProfileSchema, customerRegistrationProfileSchema, partnerRegistrationProfileSchema, registrationAddressSchema } from "../core/form-values.js";
|
|
12
|
+
import { isPluginFeatureEnabled } from "../core/plugin-features.js";
|
|
10
13
|
function cartCustomerHasAccount(cart) {
|
|
11
14
|
const customer = Reflect.get(cart, "customer");
|
|
12
15
|
return typeof customer === "object" && customer !== null && Reflect.get(customer, "has_account") === true;
|
|
@@ -14,10 +17,29 @@ function cartCustomerHasAccount(cart) {
|
|
|
14
17
|
function isVerificationRequired(result) {
|
|
15
18
|
return typeof result === "object" && result !== null && Reflect.get(result, "verification_required") === true && typeof Reflect.get(result, "token") === "string";
|
|
16
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Metadata key of the address a registration could not store yet: Medusa takes
|
|
22
|
+
* no address with the customer, and while the e-mail is unconfirmed there is
|
|
23
|
+
* no token to add one, so it waits here until the first login.
|
|
24
|
+
*/
|
|
25
|
+
const PENDING_REGISTRATION_ADDRESS_KEY = "pending_registration_address";
|
|
26
|
+
/**
|
|
27
|
+
* Metadata key of a partner application that waits for its company address
|
|
28
|
+
* (the registration address): submitted right after the address is stored.
|
|
29
|
+
*/
|
|
30
|
+
const PENDING_PARTNER_APPLICATION_KEY = "pending_partner_application";
|
|
31
|
+
function toPendingPartnerApplication(value) {
|
|
32
|
+
return typeof value === "object" && value !== null ? { confirmed_without_vat_number: Reflect.get(value, "confirmed_without_vat_number") === true } : null;
|
|
33
|
+
}
|
|
34
|
+
/** The registration address the customer create took last (Medusa answers with the whole customer). */
|
|
35
|
+
function findDefaultAddressId(customer) {
|
|
36
|
+
const addresses = customer.addresses ?? [];
|
|
37
|
+
return (addresses.find(({ is_default_billing }) => is_default_billing) ?? addresses[0])?.id ?? null;
|
|
38
|
+
}
|
|
17
39
|
function getAuthenticationToken(result) {
|
|
18
40
|
return typeof result === "string" ? result : null;
|
|
19
41
|
}
|
|
20
|
-
export function createCustomer(context, runMedusaAction) {
|
|
42
|
+
export function createCustomer(context, runMedusaAction, pluginFeatures) {
|
|
21
43
|
const { createAuthenticatedMedusaClient, createMedusaClient, session } = context;
|
|
22
44
|
async function requestCustomerVerification(email, token) {
|
|
23
45
|
const sdk = createAuthenticatedMedusaClient(token);
|
|
@@ -71,12 +93,123 @@ export function createCustomer(context, runMedusaAction) {
|
|
|
71
93
|
return undefined;
|
|
72
94
|
}
|
|
73
95
|
}
|
|
74
|
-
|
|
75
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Stores the registration address as the customer's default address.
|
|
98
|
+
* `saved` is true when stored (or nothing to store); never throws - a
|
|
99
|
+
* failing address must not turn a registration or login into a failure.
|
|
100
|
+
*/
|
|
101
|
+
async function storeRegistrationAddress(token, profile, address) {
|
|
102
|
+
const parsed = registrationAddressSchema.safeParse(address);
|
|
103
|
+
if (!parsed.success) {
|
|
104
|
+
return { addressId: null, saved: true };
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const sdk = createAuthenticatedMedusaClient(token);
|
|
108
|
+
const { vat_number: vatNumber, ...addressValues } = parsed.data;
|
|
109
|
+
const { customer } = await sdk.store.customer.createAddress({
|
|
110
|
+
...addressValues,
|
|
111
|
+
first_name: profile.first_name ?? undefined,
|
|
112
|
+
is_default_billing: true,
|
|
113
|
+
is_default_shipping: true,
|
|
114
|
+
last_name: profile.last_name ?? undefined,
|
|
115
|
+
phone: profile.phone || undefined,
|
|
116
|
+
...(vatNumber ? { metadata: vatNumberMetadataPatch(vatNumber) } : {}),
|
|
117
|
+
}, { fields: "*addresses" });
|
|
118
|
+
await sdk.store.customer.update({ metadata: { [PENDING_REGISTRATION_ADDRESS_KEY]: null } });
|
|
119
|
+
return { addressId: findDefaultAddressId(customer), saved: true };
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
context.reportError(error, { area: "customer-auth", context: { step: "registration_address" } });
|
|
123
|
+
return { addressId: null, saved: false };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Submits a registration's partner application with its company address:
|
|
128
|
+
* a UID is checked first (the application takes it only once checked; an
|
|
129
|
+
* unreachable check counts too), and the pending marker is cleared either
|
|
130
|
+
* way. True when submitted; never throws - the account stays registered.
|
|
131
|
+
*/
|
|
132
|
+
async function submitPartnerApplication(token, addressId, hasVatNumber, application) {
|
|
133
|
+
const sdk = createAuthenticatedMedusaClient(token);
|
|
134
|
+
try {
|
|
135
|
+
if (hasVatNumber && isPluginFeatureEnabled(await pluginFeatures.loadPluginFeatures(), TAX_COMPLIANCE_PLUGIN_KEY, TAX_COMPLIANCE_FEATURES.vatValidation)) {
|
|
136
|
+
await sdk.client.fetch(taxCompliancePaths.store.customers.addressTaxIdentification(addressId), { method: "POST" });
|
|
137
|
+
}
|
|
138
|
+
await sdk.client.fetch(affiliateStorePaths.post.register, {
|
|
139
|
+
body: { confirmed_without_vat_number: application.confirmed_without_vat_number, customer_address_id: addressId },
|
|
140
|
+
method: "POST",
|
|
141
|
+
});
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
context.reportError(error, { area: "affiliate", context: { step: "registration_application" } });
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
try {
|
|
150
|
+
await sdk.store.customer.update({ metadata: { [PENDING_PARTNER_APPLICATION_KEY]: null } });
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
context.reportError(error, { area: "affiliate", context: { step: "registration_application_cleanup" } });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* After a login: adds a registration address that waited for the e-mail
|
|
159
|
+
* confirmation, then the partner application that waited for it.
|
|
160
|
+
*/
|
|
161
|
+
async function applyPendingRegistrationAddress(token) {
|
|
162
|
+
try {
|
|
163
|
+
const { customer } = await createAuthenticatedMedusaClient(token).store.customer.retrieve({ fields: "first_name,last_name,phone,metadata,*addresses" });
|
|
164
|
+
const pending = customer.metadata?.[PENDING_REGISTRATION_ADDRESS_KEY];
|
|
165
|
+
if (pending && (customer.addresses?.length ?? 0) === 0) {
|
|
166
|
+
const { addressId } = await storeRegistrationAddress(token, customer, pending);
|
|
167
|
+
const application = toPendingPartnerApplication(customer.metadata?.[PENDING_PARTNER_APPLICATION_KEY]);
|
|
168
|
+
if (addressId && application) {
|
|
169
|
+
await submitPartnerApplication(token, addressId, Boolean(registrationAddressSchema.safeParse(pending).data?.vat_number), application);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
context.reportError(error, { area: "customer-auth", context: { step: "pending_registration_address" } });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function completeCustomerSignIn(token, email) {
|
|
178
|
+
const result = await completeCustomerSignInWith(token, email, {
|
|
76
179
|
createAuthenticatedClient: createAuthenticatedMedusaClient,
|
|
77
180
|
storeToken: session.setMedusaAuthToken,
|
|
78
181
|
transferCart: transferActiveCartToCustomer,
|
|
79
182
|
});
|
|
183
|
+
await applyPendingRegistrationAddress(result.sessionToken);
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
async function registerWithProfile(parsed, metadata = {}) {
|
|
187
|
+
const credentials = { email: parsed.email, password: parsed.password };
|
|
188
|
+
const registrationSdk = createMedusaClient();
|
|
189
|
+
const registrationToken = await registrationSdk.auth.register("customer", "emailpass", credentials);
|
|
190
|
+
const customerSdk = createAuthenticatedMedusaClient(registrationToken);
|
|
191
|
+
const pendingMetadata = { ...(parsed.address ? { [PENDING_REGISTRATION_ADDRESS_KEY]: parsed.address } : {}), ...metadata };
|
|
192
|
+
const { customer } = await customerSdk.store.customer.create({
|
|
193
|
+
email: parsed.email,
|
|
194
|
+
first_name: parsed.first_name,
|
|
195
|
+
last_name: parsed.last_name,
|
|
196
|
+
...(parsed.phone ? { phone: parsed.phone } : {}),
|
|
197
|
+
...(Object.keys(pendingMetadata).length > 0 ? { metadata: pendingMetadata } : {}),
|
|
198
|
+
});
|
|
199
|
+
const loginSdk = createMedusaClient();
|
|
200
|
+
const result = await loginSdk.auth.login("customer", "emailpass", credentials);
|
|
201
|
+
if (isVerificationRequired(result)) {
|
|
202
|
+
await requestCustomerVerification(parsed.email, result.token);
|
|
203
|
+
return { state: "verification_required" };
|
|
204
|
+
}
|
|
205
|
+
const token = getAuthenticationToken(result);
|
|
206
|
+
if (!token) {
|
|
207
|
+
throw new StorefrontError("auth.registration_unsupported_step");
|
|
208
|
+
}
|
|
209
|
+
await transferActiveCartToCustomer(createAuthenticatedMedusaClient(token), { id: customer.id, email: parsed.email });
|
|
210
|
+
await session.setMedusaAuthToken(token);
|
|
211
|
+
const { addressId, saved } = parsed.address ? await storeRegistrationAddress(token, parsed, parsed.address) : { addressId: null, saved: true };
|
|
212
|
+
return { addressId, addressSaved: saved, state: "success", sessionToken: token };
|
|
80
213
|
}
|
|
81
214
|
async function signInCustomer(data, { requestVerificationMail }) {
|
|
82
215
|
const parsed = customerAuthSchema.parse(data);
|
|
@@ -144,6 +277,43 @@ export function createCustomer(context, runMedusaAction) {
|
|
|
144
277
|
return { state: "success", sessionToken: token };
|
|
145
278
|
}, { endSessionOnUnauthorized: false });
|
|
146
279
|
},
|
|
280
|
+
/**
|
|
281
|
+
* Registration with name, phone and an optional address (the default
|
|
282
|
+
* address). The account counts as registered even if only the address
|
|
283
|
+
* fails (`addressSaved: false`, to add it in the account later) - a retry
|
|
284
|
+
* would otherwise end in "e-mail exists". While the e-mail is unconfirmed
|
|
285
|
+
* the address waits in the customer's metadata until the first login.
|
|
286
|
+
*/
|
|
287
|
+
async registerCustomerWithProfile(data) {
|
|
288
|
+
const parsed = customerRegistrationProfileSchema.parse(data);
|
|
289
|
+
return runMedusaAction(async () => {
|
|
290
|
+
const result = await registerWithProfile(parsed);
|
|
291
|
+
return result.state === "success" ? { addressSaved: result.addressSaved, sessionToken: result.sessionToken, state: result.state } : result;
|
|
292
|
+
}, { endSessionOnUnauthorized: false });
|
|
293
|
+
},
|
|
294
|
+
/**
|
|
295
|
+
* Partner (e.g. therapist) registration: the account with its company
|
|
296
|
+
* address as default address, then the partner application from that
|
|
297
|
+
* address. Like the plain registration it counts as registered even if a
|
|
298
|
+
* later step fails (`addressSaved` / `applicationSubmitted` false). While
|
|
299
|
+
* the e-mail is unconfirmed, address and application wait until the
|
|
300
|
+
* first login. Refused up front while the partner portal is off.
|
|
301
|
+
*/
|
|
302
|
+
async registerPartnerWithProfile(data) {
|
|
303
|
+
const parsed = partnerRegistrationProfileSchema.parse(data);
|
|
304
|
+
return runMedusaAction(async () => {
|
|
305
|
+
if (!isPluginFeatureEnabled(await pluginFeatures.loadPluginFeatures(), "affiliate", "partner_portal")) {
|
|
306
|
+
throw new StorefrontError("partner_registration.unavailable");
|
|
307
|
+
}
|
|
308
|
+
const application = { confirmed_without_vat_number: parsed.confirmed_without_vat_number };
|
|
309
|
+
const result = await registerWithProfile(parsed, { [PENDING_PARTNER_APPLICATION_KEY]: application });
|
|
310
|
+
if (result.state !== "success") {
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
const applicationSubmitted = result.addressId ? await submitPartnerApplication(result.sessionToken, result.addressId, Boolean(parsed.address.vat_number), application) : false;
|
|
314
|
+
return { addressSaved: result.addressSaved, applicationSubmitted, sessionToken: result.sessionToken, state: result.state };
|
|
315
|
+
}, { endSessionOnUnauthorized: false });
|
|
316
|
+
},
|
|
147
317
|
// The backend answers 202 for every address (no account enumeration) and
|
|
148
318
|
// sends at most one code per address and minute.
|
|
149
319
|
async requestLoginCode(data) {
|
package/dist/server/index.d.ts
CHANGED
|
@@ -61,6 +61,27 @@ export declare function createStorefrontServer(config: StorefrontServerConfig):
|
|
|
61
61
|
state: "success";
|
|
62
62
|
sessionToken: string;
|
|
63
63
|
}>>;
|
|
64
|
+
registerCustomerWithProfile(data: import("../core/form-values.js").CustomerRegistrationProfileValues): Promise<import("../core/action-result.js").MedusaActionResult<{
|
|
65
|
+
state: "verification_required";
|
|
66
|
+
addressId?: undefined;
|
|
67
|
+
addressSaved?: undefined;
|
|
68
|
+
sessionToken?: undefined;
|
|
69
|
+
} | {
|
|
70
|
+
addressSaved: boolean;
|
|
71
|
+
sessionToken: string;
|
|
72
|
+
state: "success";
|
|
73
|
+
}>>;
|
|
74
|
+
registerPartnerWithProfile(data: import("../core/form-values.js").PartnerRegistrationProfileValues): Promise<import("../core/action-result.js").MedusaActionResult<{
|
|
75
|
+
state: "verification_required";
|
|
76
|
+
addressId?: undefined;
|
|
77
|
+
addressSaved?: undefined;
|
|
78
|
+
sessionToken?: undefined;
|
|
79
|
+
} | {
|
|
80
|
+
addressSaved: boolean;
|
|
81
|
+
applicationSubmitted: boolean;
|
|
82
|
+
sessionToken: string;
|
|
83
|
+
state: "success";
|
|
84
|
+
}>>;
|
|
64
85
|
requestLoginCode(data: import("@hartl-services/medusa-contracts/types/customer-auth-store").RequestLoginCodeRequest): Promise<import("../core/action-result.js").MedusaActionResult<void>>;
|
|
65
86
|
loginWithCode(data: import("@hartl-services/medusa-contracts/types/customer-auth-store").LoginWithCodeRequest): Promise<import("../core/action-result.js").MedusaActionResult<{
|
|
66
87
|
state: "success";
|
|
@@ -100,6 +121,8 @@ export declare function createStorefrontServer(config: StorefrontServerConfig):
|
|
|
100
121
|
verifyCustomerAddressTaxIdentification(addressId: string): Promise<import("../core/action-result.js").MedusaActionResult<import("@hartl-services/medusa-contracts/types/tax-compliance-store").SubmitCustomerAddressTaxIdentificationResponse>>;
|
|
101
122
|
};
|
|
102
123
|
affiliate: {
|
|
124
|
+
loadPartnerPortalState: () => Promise<import("@hartl-services/medusa-contracts/types/affiliate-store").StoreAffiliatePortalContextResponse["portal_state"] | null>;
|
|
125
|
+
isActivePartner(): Promise<boolean>;
|
|
103
126
|
submitAffiliateApplication(data: import("@hartl-services/medusa-contracts/types/affiliate-store").SubmitAffiliateApplicationRequest): Promise<import("../core/action-result.js").MedusaActionResult<{
|
|
104
127
|
application: {
|
|
105
128
|
id: string;
|
|
@@ -236,11 +259,12 @@ export declare function createStorefrontServer(config: StorefrontServerConfig):
|
|
|
236
259
|
}): Promise<Response>;
|
|
237
260
|
};
|
|
238
261
|
catalog: {
|
|
239
|
-
listCommerceProducts({ categoryId, collectionId, countryCode, handles }: import("./catalog.js").CommerceProductListInput): Promise<import("@medusajs/types").StoreProduct[] | null>;
|
|
262
|
+
listCommerceProducts({ categoryId, collectionId, countryCode, handles, q }: import("./catalog.js").CommerceProductListInput): Promise<import("@medusajs/types").StoreProduct[] | null>;
|
|
240
263
|
commerceProductByHandle({ countryCode, handle }: {
|
|
241
264
|
countryCode: string;
|
|
242
265
|
handle: string;
|
|
243
266
|
}): Promise<import("@medusajs/types").StoreProduct | null>;
|
|
267
|
+
listCategoryTrees(): Promise<import("@medusajs/types").StoreProductCategory[]>;
|
|
244
268
|
categoryByPath(categoryPath: string[]): Promise<import("@medusajs/types").StoreProductCategory | null>;
|
|
245
269
|
collectionByHandle(handle: string): Promise<import("@medusajs/types").StoreCollection | null>;
|
|
246
270
|
};
|
package/dist/server/index.js
CHANGED
|
@@ -26,6 +26,7 @@ export function createStorefrontServer(config) {
|
|
|
26
26
|
const runMedusaAction = createRunMedusaAction(context);
|
|
27
27
|
const pluginFeatures = createPluginFeatures(context);
|
|
28
28
|
const region = createRegion(context);
|
|
29
|
+
const affiliate = createAffiliate(context, pluginFeatures, runMedusaAction);
|
|
29
30
|
return {
|
|
30
31
|
session: context.session,
|
|
31
32
|
createMedusaClient: context.createMedusaClient,
|
|
@@ -34,12 +35,12 @@ export function createStorefrontServer(config) {
|
|
|
34
35
|
pluginFeatures,
|
|
35
36
|
region,
|
|
36
37
|
cart: createCart(context, pluginFeatures),
|
|
37
|
-
customer: createCustomer(context, runMedusaAction),
|
|
38
|
+
customer: createCustomer(context, runMedusaAction, pluginFeatures),
|
|
38
39
|
checkout: createCheckout(context),
|
|
39
40
|
taxCompliance: createTaxCompliance(context, pluginFeatures, runMedusaAction),
|
|
40
|
-
affiliate
|
|
41
|
+
affiliate,
|
|
41
42
|
orders: createOrders(context),
|
|
42
43
|
documents: createDocuments(context),
|
|
43
|
-
catalog: createCatalog(context, region),
|
|
44
|
+
catalog: createCatalog(context, region, affiliate.isActivePartner),
|
|
44
45
|
};
|
|
45
46
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hartl-services/medusa-storefront",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Headless Medusa storefront logic, Next.js server/browser adapters and React hooks for shops built from the @hartl-services/medusa-* plugins.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@hartl-services/medusa-contracts": "^1.
|
|
48
|
+
"@hartl-services/medusa-contracts": "^1.2.0",
|
|
49
49
|
"server-only": "^0.0.1"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|