@hartl-services/medusa-storefront 0.2.0 → 0.4.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.
Files changed (36) hide show
  1. package/README.md +13 -0
  2. package/dist/browser/actions.d.ts +2 -0
  3. package/dist/browser/index.d.ts +32 -0
  4. package/dist/browser/query-options.d.ts +32 -0
  5. package/dist/core/affiliate/consignment-line-labels.d.ts +19 -0
  6. package/dist/core/affiliate/consignment-line-labels.js +18 -0
  7. package/dist/core/catalog/partner-only.d.ts +18 -0
  8. package/dist/core/catalog/partner-only.js +31 -0
  9. package/dist/core/form-values.d.ts +51 -2
  10. package/dist/core/form-values.js +73 -7
  11. package/dist/core/index.d.ts +2 -0
  12. package/dist/core/index.js +2 -0
  13. package/dist/core/report-error.d.ts +1 -1
  14. package/dist/core/texts-de.d.ts +2 -0
  15. package/dist/core/texts-de.js +2 -0
  16. package/dist/react/use-affiliate.d.ts +8 -0
  17. package/dist/react/use-auth.d.ts +9 -1
  18. package/dist/react/use-auth.js +35 -0
  19. package/dist/react/use-checkout-address-form.d.ts +6 -6
  20. package/dist/react/use-checkout.d.ts +4 -4
  21. package/dist/registry/account.json +2 -2
  22. package/dist/registry/affiliate.json +4 -4
  23. package/dist/registry/cart.json +1 -1
  24. package/dist/registry/checkout.json +1 -1
  25. package/dist/registry/form.json +1 -1
  26. package/dist/registry/registry.json +9 -9
  27. package/dist/registry/setup.json +3 -3
  28. package/dist/server/affiliate.d.ts +8 -1
  29. package/dist/server/affiliate.js +29 -0
  30. package/dist/server/catalog.d.ts +9 -3
  31. package/dist/server/catalog.js +46 -14
  32. package/dist/server/customer.d.ts +23 -2
  33. package/dist/server/customer.js +119 -35
  34. package/dist/server/index.d.ts +18 -0
  35. package/dist/server/index.js +4 -3
  36. package/package.json +2 -2
@@ -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, CustomerRegistrationProfileValues, 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";
@@ -40,12 +41,32 @@ export declare function createCustomer(context: ServerContext, runMedusaAction:
40
41
  */
41
42
  registerCustomerWithProfile(data: CustomerRegistrationProfileValues): Promise<import("../core/action-result.js").MedusaActionResult<{
42
43
  state: "verification_required";
44
+ addressId?: undefined;
43
45
  addressSaved?: undefined;
44
46
  sessionToken?: undefined;
45
47
  } | {
46
48
  addressSaved: boolean;
49
+ sessionToken: string;
47
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;
48
68
  sessionToken: string;
69
+ state: "success";
49
70
  }>>;
50
71
  requestLoginCode(data: RequestLoginCodeRequest): Promise<import("../core/action-result.js").MedusaActionResult<void>>;
51
72
  loginWithCode(data: LoginWithCodeRequest): Promise<import("../core/action-result.js").MedusaActionResult<{
@@ -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, customerRegistrationProfileSchema, registrationAddressSchema } 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;
@@ -20,10 +23,23 @@ function isVerificationRequired(result) {
20
23
  * no token to add one, so it waits here until the first login.
21
24
  */
22
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
+ }
23
39
  function getAuthenticationToken(result) {
24
40
  return typeof result === "string" ? result : null;
25
41
  }
26
- export function createCustomer(context, runMedusaAction) {
42
+ export function createCustomer(context, runMedusaAction, pluginFeatures) {
27
43
  const { createAuthenticatedMedusaClient, createMedusaClient, session } = context;
28
44
  async function requestCustomerVerification(email, token) {
29
45
  const sdk = createAuthenticatedMedusaClient(token);
@@ -78,40 +94,80 @@ export function createCustomer(context, runMedusaAction) {
78
94
  }
79
95
  }
80
96
  /**
81
- * Stores the registration address as the customer's default address. True
82
- * when stored (or nothing to store); never throws - a failing address must
83
- * not turn a registration or login into a failure.
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.
84
100
  */
85
101
  async function storeRegistrationAddress(token, profile, address) {
86
102
  const parsed = registrationAddressSchema.safeParse(address);
87
103
  if (!parsed.success) {
88
- return true;
104
+ return { addressId: null, saved: true };
89
105
  }
90
106
  try {
91
107
  const sdk = createAuthenticatedMedusaClient(token);
92
- await sdk.store.customer.createAddress({
93
- ...parsed.data,
108
+ const { vat_number: vatNumber, ...addressValues } = parsed.data;
109
+ const { customer } = await sdk.store.customer.createAddress({
110
+ ...addressValues,
94
111
  first_name: profile.first_name ?? undefined,
95
112
  is_default_billing: true,
96
113
  is_default_shipping: true,
97
114
  last_name: profile.last_name ?? undefined,
98
115
  phone: profile.phone || undefined,
99
- });
116
+ ...(vatNumber ? { metadata: vatNumberMetadataPatch(vatNumber) } : {}),
117
+ }, { fields: "*addresses" });
100
118
  await sdk.store.customer.update({ metadata: { [PENDING_REGISTRATION_ADDRESS_KEY]: null } });
101
- return true;
119
+ return { addressId: findDefaultAddressId(customer), saved: true };
102
120
  }
103
121
  catch (error) {
104
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" } });
105
146
  return false;
106
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
+ }
107
156
  }
108
- /** After a login: adds a registration address that waited for the e-mail confirmation. */
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
+ */
109
161
  async function applyPendingRegistrationAddress(token) {
110
162
  try {
111
163
  const { customer } = await createAuthenticatedMedusaClient(token).store.customer.retrieve({ fields: "first_name,last_name,phone,metadata,*addresses" });
112
164
  const pending = customer.metadata?.[PENDING_REGISTRATION_ADDRESS_KEY];
113
165
  if (pending && (customer.addresses?.length ?? 0) === 0) {
114
- await storeRegistrationAddress(token, customer, pending);
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
+ }
115
171
  }
116
172
  }
117
173
  catch (error) {
@@ -127,6 +183,34 @@ export function createCustomer(context, runMedusaAction) {
127
183
  await applyPendingRegistrationAddress(result.sessionToken);
128
184
  return result;
129
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 };
213
+ }
130
214
  async function signInCustomer(data, { requestVerificationMail }) {
131
215
  const parsed = customerAuthSchema.parse(data);
132
216
  return runMedusaAction(async () => {
@@ -202,32 +286,32 @@ export function createCustomer(context, runMedusaAction) {
202
286
  */
203
287
  async registerCustomerWithProfile(data) {
204
288
  const parsed = customerRegistrationProfileSchema.parse(data);
205
- const credentials = { email: parsed.email, password: parsed.password };
206
289
  return runMedusaAction(async () => {
207
- const registrationSdk = createMedusaClient();
208
- const registrationToken = await registrationSdk.auth.register("customer", "emailpass", credentials);
209
- const customerSdk = createAuthenticatedMedusaClient(registrationToken);
210
- const { customer } = await customerSdk.store.customer.create({
211
- email: parsed.email,
212
- first_name: parsed.first_name,
213
- last_name: parsed.last_name,
214
- ...(parsed.phone ? { phone: parsed.phone } : {}),
215
- ...(parsed.address ? { metadata: { [PENDING_REGISTRATION_ADDRESS_KEY]: parsed.address } } : {}),
216
- });
217
- const loginSdk = createMedusaClient();
218
- const result = await loginSdk.auth.login("customer", "emailpass", credentials);
219
- if (isVerificationRequired(result)) {
220
- await requestCustomerVerification(parsed.email, result.token);
221
- return { state: "verification_required" };
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");
222
307
  }
223
- const token = getAuthenticationToken(result);
224
- if (!token) {
225
- throw new StorefrontError("auth.registration_unsupported_step");
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;
226
312
  }
227
- await transferActiveCartToCustomer(createAuthenticatedMedusaClient(token), { id: customer.id, email: parsed.email });
228
- await session.setMedusaAuthToken(token);
229
- const addressSaved = parsed.address ? await storeRegistrationAddress(token, parsed, parsed.address) : true;
230
- return { addressSaved, state: "success", sessionToken: token };
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 };
231
315
  }, { endSessionOnUnauthorized: false });
232
316
  },
233
317
  // The backend answers 202 for every address (no account enumeration) and
@@ -63,12 +63,24 @@ export declare function createStorefrontServer(config: StorefrontServerConfig):
63
63
  }>>;
64
64
  registerCustomerWithProfile(data: import("../core/form-values.js").CustomerRegistrationProfileValues): Promise<import("../core/action-result.js").MedusaActionResult<{
65
65
  state: "verification_required";
66
+ addressId?: undefined;
66
67
  addressSaved?: undefined;
67
68
  sessionToken?: undefined;
68
69
  } | {
69
70
  addressSaved: boolean;
71
+ sessionToken: string;
70
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;
71
82
  sessionToken: string;
83
+ state: "success";
72
84
  }>>;
73
85
  requestLoginCode(data: import("@hartl-services/medusa-contracts/types/customer-auth-store").RequestLoginCodeRequest): Promise<import("../core/action-result.js").MedusaActionResult<void>>;
74
86
  loginWithCode(data: import("@hartl-services/medusa-contracts/types/customer-auth-store").LoginWithCodeRequest): Promise<import("../core/action-result.js").MedusaActionResult<{
@@ -109,6 +121,8 @@ export declare function createStorefrontServer(config: StorefrontServerConfig):
109
121
  verifyCustomerAddressTaxIdentification(addressId: string): Promise<import("../core/action-result.js").MedusaActionResult<import("@hartl-services/medusa-contracts/types/tax-compliance-store").SubmitCustomerAddressTaxIdentificationResponse>>;
110
122
  };
111
123
  affiliate: {
124
+ loadPartnerPortalState: () => Promise<import("@hartl-services/medusa-contracts/types/affiliate-store").StoreAffiliatePortalContextResponse["portal_state"] | null>;
125
+ isActivePartner(): Promise<boolean>;
112
126
  submitAffiliateApplication(data: import("@hartl-services/medusa-contracts/types/affiliate-store").SubmitAffiliateApplicationRequest): Promise<import("../core/action-result.js").MedusaActionResult<{
113
127
  application: {
114
128
  id: string;
@@ -202,6 +216,10 @@ export declare function createStorefrontServer(config: StorefrontServerConfig):
202
216
  resolved_unit_price: number | null;
203
217
  discounted_line_total: number | null;
204
218
  currency_code: string | null;
219
+ product_title: string | null;
220
+ variant_title: string | null;
221
+ batch_name: string | null;
222
+ batch_expiry_date: string | null;
205
223
  }[];
206
224
  };
207
225
  draft_order: {
@@ -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: createAffiliate(context, pluginFeatures, runMedusaAction),
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.2.0",
3
+ "version": "0.4.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.1.0",
48
+ "@hartl-services/medusa-contracts": "^1.3.0",
49
49
  "server-only": "^0.0.1"
50
50
  },
51
51
  "peerDependencies": {