@propeller-commerce/propeller-v2-react-ui 0.8.0 → 0.10.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/CHANGELOG.md CHANGED
@@ -8,6 +8,33 @@ once it reaches 1.0. Until then (the `0.x` line) the public API may change
8
8
  between minor versions; breaking changes are called out below and in
9
9
  [MIGRATION.md](./MIGRATION.md).
10
10
 
11
+ ## [0.10.0] - 2026-07-29
12
+
13
+ ### Added
14
+
15
+ - **Magic-token authentication in `useAuth`.** `magicLogin(token)` exchanges a
16
+ backend-issued magic token for a session (via
17
+ `MagicTokenService.magicTokenLogin`), sets the in-memory Bearer header — never
18
+ `setAccessToken` — and loads the viewer, mirroring `login()`. This is the
19
+ passwordless / punchout deep-link handoff. `createMagicToken(input)` issues a
20
+ magic token for a contact/customer (`MagicTokenCreateInput`, authenticated).
21
+ Both return the standard `Result` and are exposed on `UseAuthReturn`. No new
22
+ runtime dependency — the SDK's `MagicTokenService` ships in 0.14.0.
23
+
24
+ ## [0.9.0] - 2026-07-29
25
+
26
+ ### Changed
27
+
28
+ - **Align with `@propeller-commerce/propeller-sdk-v2` 0.14.0**, which removed the
29
+ entire deprecated schema surface. Category reads move to the plural localized
30
+ arrays — `Breadcrumbs` (`category.names` / `slugs`), `CategoryDescription`
31
+ (`descriptions`), `CategoryShortDescription` (`shortDescriptions`) — and the
32
+ hand-written menu query in `useMenu` now selects `names(language:)` /
33
+ `slugs(language:)`. `useClusterConfigurator` / `useProductInfo` read
34
+ `ClusterConfigSetting.attributeName` and `uuid` (were `name` / `id`); the
35
+ public `ConfiguredSetting.id` is now a `string`. Bumped the SDK dev dependency
36
+ to `^0.14.0` and `propeller-v2-core-ui` to `^0.4.0`; the runtime peer stays `*`.
37
+
11
38
  ## [0.8.0] - 2026-07-28
12
39
 
13
40
  ### Added
package/dist/index.cjs CHANGED
@@ -307,6 +307,66 @@ function useAuth(options) {
307
307
  setLoading(false);
308
308
  }
309
309
  }, [graphqlClient, language, onAuthHeaderUpdate, configuration]);
310
+ const magicLogin = React6.useCallback(async (token) => {
311
+ setLoading(true);
312
+ setError(null);
313
+ try {
314
+ const loginResult = await new propellerSdkV2.MagicTokenService(graphqlClient).magicTokenLogin(token);
315
+ const session = loginResult?.session;
316
+ const accessToken = session?.accessToken;
317
+ const refreshToken = session?.refreshToken;
318
+ const expiresAt = session?.expirationTime;
319
+ if (accessToken) {
320
+ const cfg = graphqlClient.getConfig();
321
+ graphqlClient.updateConfig({
322
+ headers: { ...cfg.headers, Authorization: `Bearer ${accessToken}` }
323
+ });
324
+ onAuthHeaderUpdate?.(accessToken);
325
+ }
326
+ const userService = propellerV2CoreUi.createServices(graphqlClient).user;
327
+ const viewerInput = {
328
+ ...configuration?.contactTrackAttributes?.length && {
329
+ contactAttributesInput: { attributeDescription: { names: configuration.contactTrackAttributes } }
330
+ },
331
+ ...configuration?.customerTrackAttributes?.length && {
332
+ customerAttributesInput: { attributeDescription: { names: configuration.customerTrackAttributes } }
333
+ },
334
+ ...configuration?.companyTrackAttributes?.length && {
335
+ companyAttributesInput: { attributeDescription: { names: configuration.companyTrackAttributes } }
336
+ },
337
+ // See isViewerSearchInput — an empty array `[]` is truthy but 400s the viewer.
338
+ ...isViewerSearchInput(configuration?.contactPAConfigInput) && {
339
+ contactPAConfigInput: configuration.contactPAConfigInput
340
+ },
341
+ ...isViewerSearchInput(configuration?.contactCompaniesSearchInput) && {
342
+ contactCompaniesSearchInput: configuration.contactCompaniesSearchInput
343
+ }
344
+ };
345
+ const viewer = await userService.getViewer(viewerInput);
346
+ const user = viewer;
347
+ return propellerV2CoreUi.ok({ user, accessToken, refreshToken, expiresAt });
348
+ } catch (e) {
349
+ const msg = e instanceof Error ? e.message : "Magic-token login failed";
350
+ setError(msg);
351
+ return propellerV2CoreUi.err(msg);
352
+ } finally {
353
+ setLoading(false);
354
+ }
355
+ }, [graphqlClient, onAuthHeaderUpdate, configuration]);
356
+ const createMagicToken = React6.useCallback(async (input) => {
357
+ setLoading(true);
358
+ setError(null);
359
+ try {
360
+ const token = await new propellerSdkV2.MagicTokenService(graphqlClient).createMagicToken(input);
361
+ return propellerV2CoreUi.ok(token);
362
+ } catch (e) {
363
+ const msg = e instanceof Error ? e.message : "Failed to create magic token";
364
+ setError(msg);
365
+ return propellerV2CoreUi.err(msg);
366
+ } finally {
367
+ setLoading(false);
368
+ }
369
+ }, [graphqlClient]);
310
370
  const registerContact = React6.useCallback(async (input, preferredLanguage = language, autoLogin = true) => {
311
371
  setLoading(true);
312
372
  setError(null);
@@ -553,7 +613,7 @@ function useAuth(options) {
553
613
  setLoading(false);
554
614
  }
555
615
  }, [graphqlClient]);
556
- return { loading, error, login, registerContact, registerCustomer, forgotPassword };
616
+ return { loading, error, login, magicLogin, createMagicToken, registerContact, registerCustomer, forgotPassword };
557
617
  }
558
618
  async function initCart(config) {
559
619
  const {
@@ -1127,7 +1187,7 @@ function useClusterConfigurator(options) {
1127
1187
  const previousSelections = {};
1128
1188
  for (let i = 0; i < settingIndex; i++) {
1129
1189
  const prev = sortedSettings[i];
1130
- if (selections[prev.name]) previousSelections[prev.name] = selections[prev.name];
1190
+ if (selections[prev.attributeName]) previousSelections[prev.attributeName] = selections[prev.attributeName];
1131
1191
  }
1132
1192
  const matching = propellerV2CoreUi.filterProductsBySelections(products, previousSelections);
1133
1193
  return propellerV2CoreUi.collectAttributeValues(matching, attributeName);
@@ -1136,43 +1196,43 @@ function useClusterConfigurator(options) {
1136
1196
  const sortedSettings = getSortedSettings();
1137
1197
  const sel = selectedAttributes;
1138
1198
  return sortedSettings.map((setting, index) => {
1139
- const availableValues = getAvailableValuesForIndexWithSelections(setting.name, index, sel);
1140
- const selectedValue = sel[setting.name] || "";
1141
- const isPreviousMissing = index > 0 && sortedSettings.slice(0, index).some((prev) => !sel[prev.name]);
1199
+ const availableValues = getAvailableValuesForIndexWithSelections(setting.attributeName, index, sel);
1200
+ const selectedValue = sel[setting.attributeName] || "";
1201
+ const isPreviousMissing = index > 0 && sortedSettings.slice(0, index).some((prev) => !sel[prev.attributeName]);
1142
1202
  const isDisabled = availableValues.length === 0 || isPreviousMissing;
1143
- let displayName = setting.name;
1203
+ let displayName = setting.attributeName;
1144
1204
  let attributeType;
1145
1205
  for (const product of products) {
1146
1206
  const items = product.attributes?.items;
1147
1207
  if (!items) continue;
1148
- const match = items.find((a) => propellerV2CoreUi.attributeNameMatches(a, setting.name));
1208
+ const match = items.find((a) => propellerV2CoreUi.attributeNameMatches(a, setting.attributeName));
1149
1209
  if (!match) continue;
1150
- displayName = propellerV2CoreUi.getAttributeDisplayName(match, language) || setting.name;
1210
+ displayName = propellerV2CoreUi.getAttributeDisplayName(match, language) || setting.attributeName;
1151
1211
  attributeType = match.attributeDescription?.type ?? void 0;
1152
1212
  break;
1153
1213
  }
1154
- return { id: setting.id, name: setting.name, displayType: setting.displayType, attributeType, priority: setting.priority, displayName, availableValues, selectedValue, disabled: isDisabled };
1214
+ return { id: setting.uuid, name: setting.attributeName, displayType: setting.displayType, attributeType, priority: setting.priority, displayName, availableValues, selectedValue, disabled: isDisabled };
1155
1215
  });
1156
1216
  }, [products, config, selectedAttributes, language]);
1157
1217
  const handleAttributeSelect = React6.useCallback(
1158
1218
  (settingName, value) => {
1159
1219
  const sortedSettings = getSortedSettings();
1160
- const changedIndex = sortedSettings.findIndex((s) => s.name === settingName);
1220
+ const changedIndex = sortedSettings.findIndex((s) => s.attributeName === settingName);
1161
1221
  if (changedIndex < 0) return;
1162
1222
  const newSelections = { ...selectedAttributes };
1163
1223
  newSelections[settingName] = value;
1164
- for (let i = changedIndex + 1; i < sortedSettings.length; i++) delete newSelections[sortedSettings[i].name];
1224
+ for (let i = changedIndex + 1; i < sortedSettings.length; i++) delete newSelections[sortedSettings[i].attributeName];
1165
1225
  for (let i = changedIndex + 1; i < sortedSettings.length; i++) {
1166
1226
  const next = sortedSettings[i];
1167
- const available = getAvailableValuesForIndexWithSelections(next.name, i, newSelections);
1227
+ const available = getAvailableValuesForIndexWithSelections(next.attributeName, i, newSelections);
1168
1228
  if (available.length > 0) {
1169
- newSelections[next.name] = available[0];
1229
+ newSelections[next.attributeName] = available[0];
1170
1230
  } else {
1171
1231
  break;
1172
1232
  }
1173
1233
  }
1174
1234
  setSelectedAttributes(newSelections);
1175
- const allSelected = sortedSettings.every((s) => !!newSelections[s.name]);
1235
+ const allSelected = sortedSettings.every((s) => !!newSelections[s.attributeName]);
1176
1236
  if (allSelected) {
1177
1237
  const matching = propellerV2CoreUi.filterProductsBySelections(products, newSelections);
1178
1238
  if (matching.length > 0 && onConfigurationChange) onConfigurationChange(matching[0]);
@@ -1188,15 +1248,15 @@ function useClusterConfigurator(options) {
1188
1248
  if (!attrItems) return;
1189
1249
  const initial = {};
1190
1250
  for (const setting of sortedSettings) {
1191
- const match = attrItems.find((a) => propellerV2CoreUi.attributeNameMatches(a, setting.name));
1251
+ const match = attrItems.find((a) => propellerV2CoreUi.attributeNameMatches(a, setting.attributeName));
1192
1252
  if (match) {
1193
1253
  const values = propellerV2CoreUi.extractAttributeValues(match);
1194
- if (values.length) initial[setting.name] = values[0];
1254
+ if (values.length) initial[setting.attributeName] = values[0];
1195
1255
  }
1196
1256
  }
1197
1257
  if (!Object.keys(initial).length) return;
1198
1258
  setSelectedAttributes(initial);
1199
- const allSelected = sortedSettings.every((s) => !!initial[s.name]);
1259
+ const allSelected = sortedSettings.every((s) => !!initial[s.attributeName]);
1200
1260
  if (allSelected && onConfigurationChange) {
1201
1261
  const matching = propellerV2CoreUi.filterProductsBySelections(products, initial);
1202
1262
  if (matching.length > 0) onConfigurationChange(matching[0]);
@@ -1532,15 +1592,15 @@ function buildCategoriesQuery(depth) {
1532
1592
  categories {
1533
1593
  categoryId
1534
1594
  hidden
1535
- name(language: $language) { value language }
1536
- slug(language: $language) { value }
1595
+ names(language: $language) { value language }
1596
+ slugs(language: $language) { value }
1537
1597
  ${buildCategoriesQuery(depth - 1)}
1538
1598
  }
1539
1599
  `;
1540
1600
  }
1541
1601
  function mapCategory(raw, language) {
1542
- const nameEntry = raw.name?.find((n) => n.language === language) ?? raw.name?.[0];
1543
- const slugEntry = raw.slug?.[0];
1602
+ const nameEntry = raw.names?.find((n) => n.language === language) ?? raw.names?.[0];
1603
+ const slugEntry = raw.slugs?.[0];
1544
1604
  return {
1545
1605
  categoryId: raw.categoryId,
1546
1606
  name: nameEntry?.value ?? "",
@@ -1620,8 +1680,8 @@ function useMenu(options) {
1620
1680
  category(categoryId: $categoryId) {
1621
1681
  categoryId
1622
1682
  hidden
1623
- name(language: $language) { value language }
1624
- slug(language: $language) { value }
1683
+ names(language: $language) { value language }
1684
+ slugs(language: $language) { value }
1625
1685
  ${buildCategoriesQuery(depth)}
1626
1686
  }
1627
1687
  }
@@ -2080,7 +2140,7 @@ function useProductInfo(options) {
2080
2140
  const service = propellerV2CoreUi.createServices(graphqlClient).cluster;
2081
2141
  const clusterConfig = await service.getClusterConfig(clusterId);
2082
2142
  const attributeNames = (clusterConfig?.config?.settings ?? []).map(
2083
- (setting) => setting.name
2143
+ (setting) => setting.attributeName
2084
2144
  );
2085
2145
  const variables = {
2086
2146
  clusterId,
@@ -6175,8 +6235,8 @@ function Breadcrumbs(props) {
6175
6235
  }
6176
6236
  const displayItems = props.showCurrent === false && items.length > 0 ? items.slice(0, items.length - 1) : items;
6177
6237
  function getCategoryName(cat) {
6178
- const match = cat.name?.find((n) => n.language === lang);
6179
- return match?.value || cat.name?.[0]?.value || "";
6238
+ const match = cat.names?.find((n) => n.language === lang);
6239
+ return match?.value || cat.names?.[0]?.value || "";
6180
6240
  }
6181
6241
  function getCategoryUrl(cat, index) {
6182
6242
  if (props.getUrl) return props.getUrl(cat, index);
@@ -6184,7 +6244,7 @@ function Breadcrumbs(props) {
6184
6244
  if (urls && typeof urls.getCategoryUrl === "function") {
6185
6245
  return urls.getCategoryUrl(cat, props.language);
6186
6246
  }
6187
- const slug = cat.slug?.find((s) => s.language === lang)?.value || cat.slug?.[0]?.value || "";
6247
+ const slug = cat.slugs?.find((s) => s.language === lang)?.value || cat.slugs?.[0]?.value || "";
6188
6248
  return `/category/${cat.categoryId}/${slug}`;
6189
6249
  }
6190
6250
  function isCurrentItem(index) {
@@ -7874,7 +7934,7 @@ function CartSummary(rawProps) {
7874
7934
  var CartSummary_default = CartSummary;
7875
7935
  function CategoryDescription(props) {
7876
7936
  const [expanded, setExpanded] = React6.useState(false);
7877
- const match = props.category?.description?.find(
7937
+ const match = props.category?.descriptions?.find(
7878
7938
  (d) => d.language === props.language
7879
7939
  );
7880
7940
  const html = match?.value || "";
@@ -7915,7 +7975,7 @@ function CategoryDescription(props) {
7915
7975
  }
7916
7976
  var CategoryDescription_default = CategoryDescription;
7917
7977
  function CategoryShortDescription(props) {
7918
- const match = props.category?.shortDescription?.find(
7978
+ const match = props.category?.shortDescriptions?.find(
7919
7979
  (d) => d.language === props.language
7920
7980
  );
7921
7981
  const html = match?.value || "";
@@ -12808,7 +12868,7 @@ function Menu(rawProps) {
12808
12868
  const lang = props.language || "NL";
12809
12869
  const category = {
12810
12870
  categoryId: cat.categoryId,
12811
- slug: [{ value: cat.slug, language: lang }]
12871
+ slugs: [{ value: cat.slug, language: lang }]
12812
12872
  };
12813
12873
  if (props.getUrl) return props.getUrl(category);
12814
12874
  return props.configuration.urls.getCategoryUrl(category, lang);
@@ -12822,8 +12882,8 @@ function Menu(rawProps) {
12822
12882
  const lang = props.language || "NL";
12823
12883
  props.onMenuItemClick({
12824
12884
  categoryId: cat.categoryId,
12825
- name: [{ value: cat.name, language: lang }],
12826
- slug: [{ value: cat.slug, language: lang }]
12885
+ names: [{ value: cat.name, language: lang }],
12886
+ slugs: [{ value: cat.slug, language: lang }]
12827
12887
  });
12828
12888
  }
12829
12889
  }