@designcrowd/library.brand-page 6.1.3 → 6.1.4-ast-prepurchase

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 (149) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-Lk8J1vHg.js → CreatedOnBrandCrowd-de-de-BFDE6VfS.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-BbXj5fb1.js → CreatedOnBrandCrowd-en-us-CRhl6-Cd.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-CjnqvYCi.js → CreatedOnBrandCrowd-es-es-VaP_-7dv.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-Dbjv4qpN.js → CreatedOnBrandCrowd-fr-ca-BySGqvWO.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-Dbjv4qpN.js → CreatedOnBrandCrowd-fr-fr-BySGqvWO.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-BZIOPEvL.js → CreatedOnBrandCrowd-pt-br-BHjXVcSk.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-BZIOPEvL.js → CreatedOnBrandCrowd-pt-pt-BHjXVcSk.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-aN4JMRDI.js → CreatedOnDcom-de-de-DIFO5IZY.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-f3VD6Pz4.js → CreatedOnDcom-en-us-CcpDa04T.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-CQhNdtRP.js → CreatedOnDcom-es-es-DHwY0lWp.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-CFsC_XoO.js → CreatedOnDcom-fr-ca-BP3P0hJL.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-J4ZtCBO9.js → CreatedOnDcom-fr-fr-Bai1giy3.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-C1yFugh_.js → CreatedOnDcom-pt-br-GgKiDrbK.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-ycOo5lDn.js → CreatedOnDcom-pt-pt-uuuxCYR3.js} +1 -1
  15. package/lib/src/PreviewModal-DY2aAN0a.js +19931 -0
  16. package/lib/src/brand-page/components/page-footer/PageFooter.mixin.d.ts +0 -1
  17. package/lib/src/brand-page/components/page-footer/PageFooter.mixin.js +1 -4
  18. package/lib/src/brand-page/models/brand-page-context.model.d.ts +2 -0
  19. package/lib/src/brand-page/models/brand-page-context.model.js +12 -0
  20. package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.d.ts +2 -0
  21. package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.js +14 -1
  22. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +134 -0
  23. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.js +50 -0
  24. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +130 -0
  25. package/lib/src/brand-page-maker/components/navigation/Navigation.mixin.d.ts +1 -0
  26. package/lib/src/brand-page-maker/components/navigation/Navigation.mixin.js +3 -0
  27. package/lib/src/brand-page-maker/components/preview/Preview.mixin.d.ts +14 -0
  28. package/lib/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.d.ts +130 -0
  29. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +130 -0
  30. package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +130 -0
  31. package/lib/src/brand-page-maker/components/seo-settings/UseExistingDomainModal.mixin.d.ts +130 -0
  32. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +130 -0
  33. package/lib/src/brand-page-maker/components/upgrade-to-premium-button/UpgradeToPremiumButton.mixin.d.ts +130 -0
  34. package/lib/src/brand-page-maker/composables/useEventTracker.d.ts +1 -0
  35. package/lib/src/brand-page-maker/composables/useEventTracker.js +1 -0
  36. package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.d.ts +132 -0
  37. package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.js +22 -6
  38. package/lib/src/bundled-translations-D42tE0D2.js +179 -0
  39. package/lib/src/bundled-translations.de-DE-D9aWGgFt.js +179 -0
  40. package/lib/src/bundled-translations.es-ES-lk4B0xQ7.js +179 -0
  41. package/lib/src/bundled-translations.fr-CA-DhOBNt6q.js +179 -0
  42. package/lib/src/bundled-translations.fr-FR-CnpafQeG.js +179 -0
  43. package/lib/src/bundled-translations.pt-BR-C4PnKrd9.js +179 -0
  44. package/lib/src/bundled-translations.pt-PT-CfKDVQC-.js +179 -0
  45. package/lib/src/bundles/bundled-translations.de-DE.json +5 -0
  46. package/lib/src/bundles/bundled-translations.es-ES.json +5 -0
  47. package/lib/src/bundles/bundled-translations.fr-CA.json +5 -0
  48. package/lib/src/bundles/bundled-translations.fr-FR.json +5 -0
  49. package/lib/src/bundles/bundled-translations.json +5 -0
  50. package/lib/src/bundles/bundled-translations.pt-BR.json +5 -0
  51. package/lib/src/bundles/bundled-translations.pt-PT.json +5 -0
  52. package/lib/src/clients/brand-page-client.d.ts +1 -0
  53. package/lib/src/composables/useTranslate.d.ts +2 -1
  54. package/lib/src/composables/useTranslate.js +2 -1
  55. package/lib/src/constants/brand-page-maker.d.ts +2 -0
  56. package/lib/src/constants/brand-page-maker.js +5 -0
  57. package/lib/src/constants/live-preview.constants.d.ts +15 -0
  58. package/lib/src/constants/live-preview.constants.js +15 -0
  59. package/lib/src/css/library.brand-page-brandCrowd.min.css +264 -121
  60. package/lib/src/css/library.brand-page-brandPage.min.css +268 -121
  61. package/lib/src/css/library.brand-page-designCom.min.css +264 -121
  62. package/lib/src/css/library.brand-page-preview.min.css +264 -121
  63. package/lib/src/index.d.ts +2 -1
  64. package/lib/src/index.mjs +102150 -225
  65. package/lib/src/lib.js +2 -1
  66. package/lib/src/preview-modal.d.ts +5 -0
  67. package/lib/src/preview-modal.js +12 -0
  68. package/lib/src/preview-modal.mjs +6 -0
  69. package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +130 -0
  70. package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +130 -0
  71. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +130 -0
  72. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +130 -0
  73. package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +144 -0
  74. package/lib/src/shared/side-bar/components/SideBar.mixin.d.ts +130 -0
  75. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +130 -0
  76. package/lib/src/tracker.mjs +1 -1
  77. package/lib/src/{useEventTracker-DmQ6AkNn.js → useEventTracker-Dmc2jFdN.js} +5 -4
  78. package/package.json +5 -1
  79. package/src/brand-page/components/button/Button.vue +11 -0
  80. package/src/brand-page/components/button/Button.vue.test.ts +90 -0
  81. package/src/brand-page/components/image-gallery/masonry/MasonryGallery.vue +1 -0
  82. package/src/brand-page/components/images-section/ImagesSection.vue +10 -0
  83. package/src/brand-page/components/media/Video.vue +60 -1
  84. package/src/brand-page/components/page-footer/PageFooter.mixin.ts +1 -4
  85. package/src/brand-page/components/page-footer/PageFooter.vue +0 -1
  86. package/src/brand-page/components/payment/Payment.vue +3 -0
  87. package/src/brand-page/components/payment/request-payment/RequestPayment.vue +1 -0
  88. package/src/brand-page/components/payment/support-me/SupportMe.vue +3 -0
  89. package/src/brand-page/components/shop/CartModal.vue +9 -0
  90. package/src/brand-page/components/shop/Shop.vue +19 -0
  91. package/src/brand-page/components/shop/Shop.vue.test.ts +182 -2
  92. package/src/brand-page/components/social-links/SocialLinks.vue +1 -9
  93. package/src/brand-page/components/social-links/SocialLinks.vue.test.ts +28 -8
  94. package/src/brand-page/components/social-links/SocialLinksItems.vue +6 -2
  95. package/src/brand-page/components/testimonial-section/TestimonialSection.vue +2 -2
  96. package/src/brand-page/components/video/Video.vue +3 -0
  97. package/src/brand-page/models/brand-page-context.model.ts +23 -0
  98. package/src/brand-page-maker/components/action-bar/ActionBar.mixin.ts +15 -0
  99. package/src/brand-page-maker/components/action-bar/ActionBarNavigation.test.ts +95 -0
  100. package/src/brand-page-maker/components/action-bar/ActionBarNavigation.vue +19 -4
  101. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.ts +57 -0
  102. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.preview-gate.test.ts +113 -0
  103. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.vue +26 -1
  104. package/src/brand-page-maker/components/navigation/Navigation.mixin.ts +3 -0
  105. package/src/brand-page-maker/components/navigation/Navigation.vue +1 -0
  106. package/src/brand-page-maker/components/preview-modal/PreviewModal.test.ts +162 -0
  107. package/src/brand-page-maker/components/preview-modal/PreviewModal.vue +347 -0
  108. package/src/brand-page-maker/components/preview-modal/PreviewModalWithState.test.ts +226 -0
  109. package/src/brand-page-maker/components/preview-modal/PreviewModalWithState.vue +149 -0
  110. package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.de-DE.json +6 -0
  111. package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.es-ES.json +6 -0
  112. package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.fr-CA.json +6 -0
  113. package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.fr-FR.json +6 -0
  114. package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.json +6 -0
  115. package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.pt-BR.json +6 -0
  116. package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.pt-PT.json +6 -0
  117. package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +29 -17
  118. package/src/brand-page-maker/composables/useEventTracker.ts +1 -0
  119. package/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.save.test.ts +106 -0
  120. package/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.ts +40 -8
  121. package/src/bundles/bundled-translations.de-DE.json +5 -0
  122. package/src/bundles/bundled-translations.es-ES.json +5 -0
  123. package/src/bundles/bundled-translations.fr-CA.json +5 -0
  124. package/src/bundles/bundled-translations.fr-FR.json +5 -0
  125. package/src/bundles/bundled-translations.json +5 -0
  126. package/src/bundles/bundled-translations.pt-BR.json +5 -0
  127. package/src/bundles/bundled-translations.pt-PT.json +5 -0
  128. package/src/clients/brand-page-client.ts +2 -0
  129. package/src/composables/useTranslate.ts +2 -0
  130. package/src/constants/brand-page-maker.ts +5 -0
  131. package/src/constants/live-preview.constants.ts +15 -0
  132. package/src/lib.ts +5 -1
  133. package/src/preview-modal.ts +14 -0
  134. package/src/shared/common/i18n/common.de-DE.json +1 -0
  135. package/src/shared/common/i18n/common.es-ES.json +1 -0
  136. package/src/shared/common/i18n/common.fr-CA.json +1 -0
  137. package/src/shared/common/i18n/common.fr-FR.json +1 -0
  138. package/src/shared/common/i18n/common.json +1 -0
  139. package/src/shared/common/i18n/common.pt-BR.json +1 -0
  140. package/src/shared/common/i18n/common.pt-PT.json +1 -0
  141. package/src/shared/ecommerce-settings/store/cart-store.test.ts +38 -0
  142. package/lib/src/bundled-translations-2HkRBbBD.js +0 -177
  143. package/lib/src/bundled-translations.de-DE-CL9ZEhU2.js +0 -177
  144. package/lib/src/bundled-translations.es-ES-uj7Sliu0.js +0 -177
  145. package/lib/src/bundled-translations.fr-CA-BMnBtr_w.js +0 -177
  146. package/lib/src/bundled-translations.fr-FR-9PEZedzq.js +0 -177
  147. package/lib/src/bundled-translations.pt-BR-B3oNDkoe.js +0 -177
  148. package/lib/src/bundled-translations.pt-PT-DM7Vsf-h.js +0 -177
  149. package/lib/src/lib-CVwyHyBz.js +0 -121538
@@ -17,7 +17,6 @@
17
17
  />
18
18
  <SocialLinksItems
19
19
  :id="id"
20
- :is-preview="isPreview"
21
20
  :container-style="iconContainerStyle"
22
21
  :icon-style="iconStyle"
23
22
  :links="links"
@@ -33,10 +32,7 @@ import TextWrapper from '../text-wrapper/TextWrapper.vue';
33
32
  import type { SocialLinksViewModel } from 'src/brand-page/view-models';
34
33
  import { SOCIAL_LINKS_ICON_DISPLAY_STYLES } from '@brand-page/models';
35
34
  import type { BrandPageContext, LinksItem } from '@brand-page/models';
36
- import {
37
- contentPaddingStyle as toContentPaddingStyle,
38
- isPreviewMode,
39
- } from '@brand-page/utils';
35
+ import { contentPaddingStyle as toContentPaddingStyle } from '@brand-page/utils';
40
36
  import { computed } from 'vue';
41
37
 
42
38
  const props = withDefaults(
@@ -89,10 +85,6 @@ const id = computed((): string => {
89
85
  return props.viewModel.id;
90
86
  });
91
87
 
92
- const isPreview = computed((): boolean => {
93
- return isPreviewMode(props.context);
94
- });
95
-
96
88
  const links = computed((): LinksItem[] => {
97
89
  return props.viewModel.links;
98
90
  });
@@ -115,7 +115,6 @@ describe('Social Links', () => {
115
115
  id: faker.datatype.uuid(),
116
116
  containerStyle: {},
117
117
  iconStyle: {},
118
- isPreview: false,
119
118
  links: [
120
119
  {
121
120
  id: 'social-link-facebook',
@@ -137,16 +136,13 @@ describe('Social Links', () => {
137
136
  expect(anchor.attributes()['href']).toBe(undefined);
138
137
  });
139
138
 
140
- test('href attribute should match url', () => {
141
- const context = createTestBrandPageContext();
142
-
143
- const wrapper = shallowMount(SocialLinksItems, {
139
+ const mountWithContext = (context: BrandPageContext) =>
140
+ shallowMount(SocialLinksItems, {
144
141
  propsData: {
145
142
  context,
146
143
  id: faker.datatype.uuid(),
147
144
  containerStyle: {},
148
145
  iconStyle: {},
149
- isPreview: false,
150
146
  links: [
151
147
  {
152
148
  id: 'social-link-facebook',
@@ -163,8 +159,32 @@ describe('Social Links', () => {
163
159
  },
164
160
  });
165
161
 
166
- const anchor = wrapper.find('.c-social-links__links-container li a');
162
+ const hrefOf = (wrapper: ReturnType<typeof shallowMount>) =>
163
+ wrapper.find('.c-social-links__links-container li a').attributes()['href'];
164
+
165
+ // Published site: no preview flags — the real href renders. (unchanged)
166
+ test('href attribute should match url', () => {
167
+ expect(hrefOf(mountWithContext(createTestBrandPageContext()))).toBe(
168
+ 'https://www.fb.com',
169
+ );
170
+ });
171
+
172
+ // Editor/maker canvas: href stripped so a designer can't click out. (unchanged)
173
+ test('editor preview (context.isPreview) strips the href', () => {
174
+ const context = { ...createTestBrandPageContext(), isPreview: true };
175
+
176
+ expect(hrefOf(mountWithContext(context))).toBe(undefined);
177
+ });
178
+
179
+ // Customer live preview: href now RENDERS (BP-5959) so internal links resolve
180
+ // in-frame; external ones are blocked by the web-renderer preview gate instead.
181
+ test('live preview (context.isLivePreview) renders the href', () => {
182
+ const context = {
183
+ ...createTestBrandPageContext(),
184
+ isPreview: false,
185
+ isLivePreview: true,
186
+ };
167
187
 
168
- expect(anchor.attributes()['href']).toBe('https://www.fb.com');
188
+ expect(hrefOf(mountWithContext(context))).toBe('https://www.fb.com');
169
189
  });
170
190
  });
@@ -45,7 +45,6 @@ interface Props {
45
45
  id: string;
46
46
  containerStyle: BrandPageCssStyle;
47
47
  iconStyle: BrandPageCssStyle;
48
- isPreview: boolean;
49
48
  links: LinksItem[];
50
49
  context: BrandPageContext;
51
50
  contentIndex: number | undefined;
@@ -62,7 +61,12 @@ const isUrl = computed(() => {
62
61
  });
63
62
 
64
63
  const getValidInput = (value: string): string | undefined => {
65
- if (props.isPreview || !value) return undefined;
64
+ // Strip the href in the EDITOR preview only (`context.isPreview`), so a
65
+ // designer can't click out of the canvas. In the customer LIVE preview
66
+ // (`context.isLivePreview`) the href stays so internal links resolve in-frame
67
+ // (see web-renderer resolveInternalPreviewPaths); external ones get blocked by
68
+ // the preview gate anyway. (BP-5959)
69
+ if (props.context.isPreview || !value) return undefined;
66
70
 
67
71
  if (isEmail(value)) {
68
72
  return `mailto:${value}`;
@@ -33,7 +33,7 @@
33
33
  :hide-bullets-on-mobile="false"
34
34
  >
35
35
  <template #arrow-left>
36
- <div id="button-prev">
36
+ <div id="button-prev" data-preview-interactive>
37
37
  <Icon
38
38
  classes="sm:tw-hidden tw--mb-1 tw-mr-2 tw-cursor-pointer"
39
39
  name="chevron-left"
@@ -120,7 +120,7 @@
120
120
  </div>
121
121
  </template>
122
122
  <template #arrow-right>
123
- <div id="button-next">
123
+ <div id="button-next" data-preview-interactive>
124
124
  <Icon
125
125
  classes="sm:tw-hidden tw--mb-1 tw-ml-2 tw-cursor-pointer"
126
126
  name="chevron-right"
@@ -8,7 +8,10 @@
8
8
  :class="{ 'tw-pb-4': isCollapsed, 'tw-pb-0.5': !isCollapsed }"
9
9
  :style="containerStyle"
10
10
  >
11
+ <!-- Video rolls its own header, so the `collapsible` allow-list entry can't
12
+ reach it. Expanding is viewing, not acting. -->
11
13
  <div
14
+ data-preview-interactive
12
15
  class="tw-relative tw-text-center tw-w-full hover:tw-cursor-pointer"
13
16
  :class="{ 'tw-mb-4': !isCollapsed }"
14
17
  @click="onHeaderToggleClick"
@@ -49,11 +49,18 @@ export interface AbTestResult {
49
49
  const AB_TEST_KEYS = {
50
50
  BP_3926_FREEMIUM_WATERMARK: 'bc-bp-3926-freemium-cta-in-website-editor',
51
51
  BP_5245_AI_WEBSITE_ASSISTANT: 'bc-bp-5245-ai-website-assistant-postpurchase',
52
+ // Two separate live-preview experiments — bc-net sends only the one for the
53
+ // current purchase state, and a user's value on each side is independent
54
+ // (BP-5873). Keys must match `CampaignNames.LivePreview*Test` in bc-net.
55
+ BP_5873_LIVE_PREVIEW_PREPURCHASE: 'bc-bp-5873-live-preview-prepurchase',
56
+ BP_5873_LIVE_PREVIEW_POSTPURCHASE: 'bc-bp-5873-live-preview-postpurchase',
52
57
  };
53
58
 
54
59
  export interface AbTest {
55
60
  bp_3926_freemium_watermark?: AbTestResult;
56
61
  bp_5245_ai_website_assistant?: AbTestResult;
62
+ bp_5873_live_preview_prepurchase?: AbTestResult;
63
+ bp_5873_live_preview_postpurchase?: AbTestResult;
57
64
  }
58
65
 
59
66
  export const MEDIA_SIZES = {
@@ -143,5 +150,21 @@ export const getAbTestResults = (abtestKeyValues: AbTest): AbTest => {
143
150
  );
144
151
  }
145
152
 
153
+ // AB Test for BP-5873 - Live preview (pre- and post-purchase run separately)
154
+
155
+ if (abtestKeyValues[AB_TEST_KEYS.BP_5873_LIVE_PREVIEW_PREPURCHASE]) {
156
+ abTests.bp_5873_live_preview_prepurchase = getValueType(
157
+ AB_TEST_KEYS.BP_5873_LIVE_PREVIEW_PREPURCHASE,
158
+ abtestKeyValues[AB_TEST_KEYS.BP_5873_LIVE_PREVIEW_PREPURCHASE],
159
+ );
160
+ }
161
+
162
+ if (abtestKeyValues[AB_TEST_KEYS.BP_5873_LIVE_PREVIEW_POSTPURCHASE]) {
163
+ abTests.bp_5873_live_preview_postpurchase = getValueType(
164
+ AB_TEST_KEYS.BP_5873_LIVE_PREVIEW_POSTPURCHASE,
165
+ abtestKeyValues[AB_TEST_KEYS.BP_5873_LIVE_PREVIEW_POSTPURCHASE],
166
+ );
167
+ }
168
+
146
169
  return abTests;
147
170
  };
@@ -2,6 +2,7 @@ import { EVENTS } from '../../../constants/brand-page-maker';
2
2
  import {
3
3
  useBrandPageMakerUserStore,
4
4
  useBrandPageMakerStore,
5
+ useBrandPageContainerStore,
5
6
  } from '@brand-page-maker/stores';
6
7
  import { computed, defineComponent } from 'vue';
7
8
  import { UPGRADE_CTA_LOCATIONS } from './../../composables/useEventTracker';
@@ -12,9 +13,20 @@ export default defineComponent({
12
13
  setup() {
13
14
  const userStore = useBrandPageMakerUserStore();
14
15
  const brandPageMakerStore = useBrandPageMakerStore();
16
+ const containerStore = useBrandPageContainerStore();
15
17
 
16
18
  return {
17
19
  isAdmin: computed(() => userStore.isAdmin),
20
+ // BP-5873: the Preview button is behind an AB test, and pre/post purchase are
21
+ // two separate experiments. Pick the one for the current purchase state — a
22
+ // user's value on the other side is independent and must not leak across.
23
+ isLivePreviewEnabled: computed(() => {
24
+ const experiment = containerStore.isPrePurchase
25
+ ? containerStore.abTests.bp_5873_live_preview_prepurchase
26
+ : containerStore.abTests.bp_5873_live_preview_postpurchase;
27
+
28
+ return !!experiment?.isVariation;
29
+ }),
18
30
  isSaving: computed(() => brandPageMakerStore.isSaving),
19
31
  isPublished: computed(() => brandPageMakerStore.isPublished),
20
32
  isPublishButtonEnabled: computed(
@@ -32,6 +44,9 @@ export default defineComponent({
32
44
  saveBrandPage() {
33
45
  this.$emit(EVENTS.Save);
34
46
  },
47
+ previewBrandPage() {
48
+ this.$emit(EVENTS.Preview);
49
+ },
35
50
  saveAndPublishBrandPage(isActionBar: boolean) {
36
51
  if (!this.isPublishButtonEnabled) {
37
52
  return;
@@ -0,0 +1,95 @@
1
+ import { describe, test, expect, beforeEach } from 'vitest';
2
+ import { createPinia, setActivePinia } from 'pinia';
3
+ import { shallowMount } from '@vue/test-utils';
4
+ import ActionBarNavigation from './ActionBarNavigation.vue';
5
+ import { useBrandPageContainerStore } from '@brand-page-maker/stores';
6
+ import { getAbTestResults } from '@brand-page/models/brand-page-context.model';
7
+
8
+ // Must match `CampaignNames.LivePreview*` in bc-net. The two experiments share
9
+ // one set of variation values — they're told apart by campaign key, not value.
10
+ const PREPURCHASE = 'bc-bp-5873-live-preview-prepurchase';
11
+ const POSTPURCHASE = 'bc-bp-5873-live-preview-postpurchase';
12
+ const VARIATION_1 = 'bp-5873-variation-1';
13
+ const CONTROL = 'bp-5873-control';
14
+
15
+ const mountBar = () => shallowMount(ActionBarNavigation);
16
+
17
+ const hasPreviewButton = (wrapper: ReturnType<typeof mountBar>) =>
18
+ wrapper.find('[data-test-preview-button]').exists();
19
+
20
+ // The AB value arrives as the raw bc-net campaign string, so go through the real
21
+ // parser rather than hand-building an AbTestResult — that's the contract under test.
22
+ const setAbTest = (
23
+ store: ReturnType<typeof useBrandPageContainerStore>,
24
+ key: string,
25
+ value: string,
26
+ ) => {
27
+ store.abTests = getAbTestResults({ [key]: value } as never);
28
+ };
29
+
30
+ describe('ActionBarNavigation — BP-5873 preview AB test', () => {
31
+ let containerStore: ReturnType<typeof useBrandPageContainerStore>;
32
+
33
+ beforeEach(() => {
34
+ setActivePinia(createPinia());
35
+ containerStore = useBrandPageContainerStore();
36
+ });
37
+
38
+ test('hides Preview when no AB value has been sent', () => {
39
+ expect(hasPreviewButton(mountBar())).toBe(false);
40
+ });
41
+
42
+ test('post-purchase: shows Preview on variation-1, hides it on control', () => {
43
+ containerStore.isPrePurchase = false;
44
+
45
+ setAbTest(containerStore, POSTPURCHASE, VARIATION_1);
46
+ expect(hasPreviewButton(mountBar())).toBe(true);
47
+
48
+ setAbTest(containerStore, POSTPURCHASE, CONTROL);
49
+ expect(hasPreviewButton(mountBar())).toBe(false);
50
+ });
51
+
52
+ test('pre-purchase: shows Preview on variation-1, hides it on control', () => {
53
+ containerStore.isPrePurchase = true;
54
+
55
+ setAbTest(containerStore, PREPURCHASE, VARIATION_1);
56
+ expect(hasPreviewButton(mountBar())).toBe(true);
57
+
58
+ setAbTest(containerStore, PREPURCHASE, CONTROL);
59
+ expect(hasPreviewButton(mountBar())).toBe(false);
60
+ });
61
+
62
+ // Two separate experiments, so a value on one side must never satisfy the other —
63
+ // otherwise a post-purchase variation would leak the button into pre-purchase.
64
+ test('the two experiments never read each other', () => {
65
+ containerStore.isPrePurchase = true;
66
+ setAbTest(containerStore, POSTPURCHASE, VARIATION_1);
67
+ expect(hasPreviewButton(mountBar())).toBe(false);
68
+
69
+ containerStore.isPrePurchase = false;
70
+ setAbTest(containerStore, PREPURCHASE, VARIATION_1);
71
+ expect(hasPreviewButton(mountBar())).toBe(false);
72
+ });
73
+
74
+ // The mobile nav row is equal columns: 3 with Save, 2 once the site is published and
75
+ // Save drops out. Preview must be a real column like the rest — while it was content
76
+ // width, Publish ate the whole remaining row on a published site.
77
+ test('Preview shares the mobile row equally with the other buttons', () => {
78
+ containerStore.isPrePurchase = false;
79
+ setAbTest(containerStore, POSTPURCHASE, VARIATION_1);
80
+ containerStore.brandPageStatus = 'Published';
81
+
82
+ const wrapper = mountBar();
83
+ const buttons = wrapper.findAllComponents({ name: 'Button' });
84
+
85
+ // Save is gone once published, so this is the two-column case.
86
+ expect(buttons).toHaveLength(2);
87
+ expect(hasPreviewButton(wrapper)).toBe(true);
88
+
89
+ // Every visible column carries the same width contract, so they halve the row.
90
+ buttons.forEach((button) => {
91
+ expect(button.classes()).toContain('tw-w-full');
92
+ expect(button.classes()).toContain('lg:tw-w-auto');
93
+ });
94
+ });
95
+ });
@@ -1,8 +1,21 @@
1
1
  <template>
2
2
  <div
3
3
  data-component="action-bar-navigation"
4
- class="tw-flex tw-items-center tw-border-t tw-border-solid tw-border-grayscale-600 tw-h-12 tw-px-2 lg:tw-px-0 lg:tw-h-auto lg:tw-border-none tw-gap-5 tw-w-full lg:tw-justify-end tw-box-border"
4
+ class="tw-flex tw-items-center tw-border-t tw-border-solid tw-border-grayscale-600 tw-h-12 tw-px-2 lg:tw-px-0 lg:tw-h-auto lg:tw-border-none tw-gap-3 lg:tw-gap-5 tw-w-full lg:tw-justify-end tw-box-border"
5
5
  >
6
+ <Button
7
+ v-if="!isAdmin && isLivePreviewEnabled"
8
+ :label="constantsTr('previewDisplayName')"
9
+ variant="no-border"
10
+ :white="true"
11
+ size="small-medium"
12
+ icon-left="preview"
13
+ icon-left-size="sm"
14
+ class="tw-w-full lg:tw-w-auto tw-inline-flex"
15
+ classes="tw-w-full lg:tw-w-auto tw-whitespace-nowrap !tw-px-0 lg:!tw-px-4"
16
+ data-test-preview-button
17
+ @on-click.stop="previewBrandPage"
18
+ />
6
19
  <Button
7
20
  v-if="!isPublished"
8
21
  :label="
@@ -13,7 +26,7 @@
13
26
  :white="true"
14
27
  size="small-medium"
15
28
  class="tw-w-full lg:tw-w-auto"
16
- classes="tw-px-6 tw-w-full lg:tw-w-auto"
29
+ classes="!tw-px-3 lg:!tw-px-6 tw-w-full lg:tw-w-auto tw-whitespace-nowrap"
17
30
  @on-click.stop="saveBrandPage"
18
31
  />
19
32
  <Button
@@ -33,7 +46,7 @@
33
46
  :disabled="!isPublishButtonEnabled"
34
47
  size="small-medium"
35
48
  class="tw-w-full lg:tw-w-auto"
36
- classes="tw-px-6 tw-w-full lg:tw-w-auto"
49
+ classes="!tw-px-3 lg:!tw-px-6 tw-w-full lg:tw-w-auto tw-whitespace-nowrap"
37
50
  @on-click.stop="saveAndPublishBrandPage(true)"
38
51
  />
39
52
  </div>
@@ -41,7 +54,8 @@
41
54
  <script>
42
55
  import { Button } from '@designcrowd/fe-shared-lib';
43
56
  import ActionBarMixin from './ActionBar.mixin.ts';
44
- import { actionBarTr } from '@src/composables/useTranslate';
57
+ // Preview reuses the translated sidebar tab label — no new Smartling string.
58
+ import { actionBarTr, constantsTr } from '@src/composables/useTranslate';
45
59
 
46
60
  export default {
47
61
  components: {
@@ -52,6 +66,7 @@ export default {
52
66
  return {
53
67
  ...ActionBarMixin.setup(props, context),
54
68
  actionBarTr,
69
+ constantsTr,
55
70
  };
56
71
  },
57
72
  };
@@ -177,6 +177,7 @@ export default defineComponent({
177
177
  mounted: false,
178
178
  debounceOnResize: undefined as DebounceHandle | undefined,
179
179
  showSaveSuccessMessage: false,
180
+ isPreviewModalVisible: false,
180
181
  };
181
182
  },
182
183
  computed: {
@@ -303,6 +304,24 @@ export default defineComponent({
303
304
 
304
305
  this.$emit(EVENTS.Initialised);
305
306
  },
307
+ watch: {
308
+ // Pre-purchase only: the host's save has produced the signed link, so the frame
309
+ // can open with it already in hand — no waiting inside the modal. No link means
310
+ // the save failed or the customer was sent to register, and the editor has
311
+ // already said so, so nothing opens.
312
+ 'containerStore.livePreviewUrl'(livePreviewUrl?: string) {
313
+ if (
314
+ !livePreviewUrl ||
315
+ !this.containerStore.isPrePurchase ||
316
+ this.isPreviewModalVisible
317
+ ) {
318
+ return;
319
+ }
320
+
321
+ this.isPreviewModalVisible = true;
322
+ this.$emit(EVENTS.Preview);
323
+ },
324
+ },
306
325
  mounted() {
307
326
  if (window) {
308
327
  window.removeEventListener('resize', this.onResizeWithDebounce);
@@ -434,9 +453,47 @@ export default defineComponent({
434
453
  : UPGRADE_CTA_LOCATIONS.EDITOR_PUBLISH_TOP,
435
454
  });
436
455
  },
456
+ onPreviewPublish() {
457
+ // Same host publish flow as the editor button, tracked as its own location.
458
+ this.$emit(EVENTS.Publish, {
459
+ eventButtonLocation: UPGRADE_CTA_LOCATIONS.EDITOR_PREVIEW_PUBLISH,
460
+ });
461
+ },
437
462
  onBackBrandPage() {
438
463
  this.$emit(EVENTS.Back);
439
464
  },
465
+ onPreview() {
466
+ // The preview frames a SAVED, owned draft. A signed-out customer can't have
467
+ // one, and the save needs an owner — so sign-in comes first, via the host's
468
+ // Save Progress flow.
469
+ if (!this.isUserAuthenticated) {
470
+ this.$emit(EVENTS.LivePreviewClicked);
471
+
472
+ return;
473
+ }
474
+
475
+ // Pre-purchase drafts are saved by the HOST (`prepurchase/upsert`), never by
476
+ // the maker store. Ask for that save and stay closed until it hands back a
477
+ // preview URL (see the `livePreviewUrl` watcher). Opening first would mean
478
+ // waiting inside the modal on a save it cannot see the outcome of, which is
479
+ // how the loader used to hang forever when a save failed or asked the customer
480
+ // to register (BP-5870). A failed save now surfaces the editor's own error and
481
+ // nothing opens.
482
+ if (this.containerStore.isPrePurchase) {
483
+ this.containerStore.livePreviewUrl = undefined;
484
+ this.$emit(EVENTS.LivePreviewClicked);
485
+
486
+ return;
487
+ }
488
+
489
+ // Post-purchase saves through the maker store, which reports its own result —
490
+ // so the modal can open first and show its loader while that runs.
491
+ this.isPreviewModalVisible = true;
492
+ this.$emit(EVENTS.Preview);
493
+ },
494
+ onClosePreview() {
495
+ this.isPreviewModalVisible = false;
496
+ },
440
497
  onChangeTemplateConfirmModalCancel() {
441
498
  this.brandPageMakerStore.onChangeTemplateConfirmModalCancel();
442
499
  },
@@ -0,0 +1,113 @@
1
+ import { describe, test, expect, vi } from 'vitest';
2
+ import BrandPageMakerMixin from './BrandPageMaker.mixin';
3
+ import { EVENTS } from '../../../constants/brand-page-maker';
4
+
5
+ // The preview frames a SAVED, owned draft, so who saves it decides when the modal
6
+ // may open. Signed out: nobody owns a draft, so hand off to the host's Save Progress
7
+ // flow. Post-purchase: the modal saves through the maker store itself, so it can open
8
+ // first and show its loader.
9
+ //
10
+ // Pre-purchase: the HOST owns the save, so nothing opens until that save hands back a
11
+ // signed URL — the `livePreviewUrl` watcher is what opens the modal.
12
+ //
13
+ // Mounting the whole editor to reach these branches would dwarf what is tested, so the
14
+ // mixin's methods and watcher are driven directly.
15
+ const onPreview = (BrandPageMakerMixin as any).methods.onPreview;
16
+ const onLivePreviewUrlChanged = (BrandPageMakerMixin as any).watch[
17
+ 'containerStore.livePreviewUrl'
18
+ ];
19
+
20
+ const createContext = (
21
+ overrides: {
22
+ isUserAuthenticated?: boolean;
23
+ isPreviewModalVisible?: boolean;
24
+ containerStore?: { isPrePurchase: boolean; livePreviewUrl?: string };
25
+ } = {},
26
+ ) => ({
27
+ isUserAuthenticated: true,
28
+ isPreviewModalVisible: false,
29
+ containerStore: { isPrePurchase: false, livePreviewUrl: undefined },
30
+ $emit: vi.fn(),
31
+ ...overrides,
32
+ });
33
+
34
+ describe('BrandPageMaker.onPreview', () => {
35
+ test('a signed-out customer is sent through Save Progress instead', () => {
36
+ const context = createContext({ isUserAuthenticated: false });
37
+
38
+ onPreview.call(context);
39
+
40
+ expect(context.$emit).toHaveBeenCalledWith(EVENTS.LivePreviewClicked);
41
+ expect(context.isPreviewModalVisible).toBe(false);
42
+ });
43
+
44
+ test('post-purchase opens the preview, which saves through the maker store', () => {
45
+ const context = createContext();
46
+
47
+ onPreview.call(context);
48
+
49
+ expect(context.isPreviewModalVisible).toBe(true);
50
+ expect(context.$emit).toHaveBeenCalledWith(EVENTS.Preview);
51
+ expect(context.$emit).not.toHaveBeenCalledWith(EVENTS.LivePreviewClicked);
52
+ });
53
+
54
+ // Opening here would mean waiting inside the modal on a host save it can't see the
55
+ // outcome of — which is how the loader used to hang forever (BP-5870).
56
+ test('pre-purchase asks the host to save and stays closed', () => {
57
+ const context = createContext({
58
+ containerStore: { isPrePurchase: true, livePreviewUrl: 'https://stale' },
59
+ });
60
+
61
+ onPreview.call(context);
62
+
63
+ expect(context.isPreviewModalVisible).toBe(false);
64
+ expect(context.$emit).toHaveBeenCalledWith(EVENTS.LivePreviewClicked);
65
+ expect(context.$emit).not.toHaveBeenCalledWith(EVENTS.Preview);
66
+ // A stale URL from an earlier preview must not open the frame on the next click.
67
+ expect(context.containerStore.livePreviewUrl).toBeUndefined();
68
+ });
69
+ });
70
+
71
+ describe('BrandPageMaker livePreviewUrl watcher', () => {
72
+ test('opens the preview once the host save hands back a URL', () => {
73
+ const context = createContext({
74
+ containerStore: { isPrePurchase: true, livePreviewUrl: undefined },
75
+ });
76
+
77
+ onLivePreviewUrlChanged.call(
78
+ context,
79
+ 'https://brand.site/_p/request?token=x',
80
+ );
81
+
82
+ expect(context.isPreviewModalVisible).toBe(true);
83
+ expect(context.$emit).toHaveBeenCalledWith(EVENTS.Preview);
84
+ });
85
+
86
+ // A failed save or a customer sent to register never sets a URL, and the editor has
87
+ // already surfaced that — so nothing should open.
88
+ test('stays closed when the save produced no URL', () => {
89
+ const context = createContext({
90
+ containerStore: { isPrePurchase: true, livePreviewUrl: undefined },
91
+ });
92
+
93
+ onLivePreviewUrlChanged.call(context, undefined);
94
+
95
+ expect(context.isPreviewModalVisible).toBe(false);
96
+ expect(context.$emit).not.toHaveBeenCalledWith(EVENTS.Preview);
97
+ });
98
+
99
+ // Post-purchase opens the modal itself, so the watcher must not double-open it.
100
+ test('ignores post-purchase, which opens the modal itself', () => {
101
+ const context = createContext({
102
+ containerStore: { isPrePurchase: false, livePreviewUrl: undefined },
103
+ });
104
+
105
+ onLivePreviewUrlChanged.call(
106
+ context,
107
+ 'https://brand.site/_p/request?token=x',
108
+ );
109
+
110
+ expect(context.isPreviewModalVisible).toBe(false);
111
+ expect(context.$emit).not.toHaveBeenCalledWith(EVENTS.Preview);
112
+ });
113
+ });
@@ -15,6 +15,7 @@
15
15
  @back="onBackBrandPage"
16
16
  @save="saveBrandPage('Navigation')"
17
17
  @publish="saveAndPublishBrandPage(false)"
18
+ @preview="onPreview"
18
19
  @create-team-cards="onCreateTeamCards"
19
20
  />
20
21
  <slot name="editButtonHeader" />
@@ -148,6 +149,12 @@
148
149
  @cancel="onChangeTemplateConfirmModalCancel"
149
150
  />
150
151
  <!-- End template change confirm modal -->
152
+
153
+ <PreviewModalWithState
154
+ :visible="isPreviewModalVisible"
155
+ @publish="onPreviewPublish"
156
+ @close="onClosePreview"
157
+ />
151
158
  </div>
152
159
  </template>
153
160
  <script>
@@ -171,6 +178,7 @@ import AdminLogoSettings from '../admin-logo-settings/AdminLogoSettings.vue';
171
178
  import AdminBackgroundSettings from '../admin-background-settings/AdminBackgroundSettings.vue';
172
179
  import ConfirmModal from '@shared/confirm-modal/ConfirmModal.vue';
173
180
  import ProductConfigModal from '@brand-page-maker/components/shop/ProductConfigModal.vue';
181
+ import PreviewModalWithState from '../preview-modal/PreviewModalWithState.vue';
174
182
  import VideoPlayButton from '../video-play/VideoPlayButton.vue';
175
183
 
176
184
  import { SIDE_BAR_TAB_NAMES } from '@brand-page-maker/constants';
@@ -205,6 +213,7 @@ export default {
205
213
  SideBar,
206
214
  StyleSettings,
207
215
  ProductConfigModal,
216
+ PreviewModalWithState,
208
217
  VideoPlayButton,
209
218
  TemplatesSettings: BrandPageMakerTemplateSettings,
210
219
  SparkleIcon,
@@ -215,8 +224,24 @@ export default {
215
224
  type: Boolean,
216
225
  default: false,
217
226
  },
227
+ // Whether the current user is signed in. When false, clicking Preview asks
228
+ // the host to prompt sign-in first (the editor preview saves + mints an
229
+ // owner-gated token) instead of opening it. Defaults true so hosts that
230
+ // don't pass it keep the previous behaviour.
231
+ isUserAuthenticated: {
232
+ type: Boolean,
233
+ default: true,
234
+ },
218
235
  },
219
- emits: ['contactsupportclicked', 'initialised', 'save', 'publish', 'Back'],
236
+ emits: [
237
+ 'contactsupportclicked',
238
+ 'initialised',
239
+ 'save',
240
+ 'publish',
241
+ 'preview',
242
+ 'livepreviewclicked',
243
+ 'Back',
244
+ ],
220
245
  setup(props, context) {
221
246
  const localisedRouteStore = useLocalisedRouteStore();
222
247
  const brandPageMakerStore = useBrandPageMakerStore();
@@ -48,6 +48,9 @@ export default defineComponent({
48
48
  saveAndPublishBrandPage() {
49
49
  this.$emit(EVENTS.Publish);
50
50
  },
51
+ previewBrandPage() {
52
+ this.$emit(EVENTS.Preview);
53
+ },
51
54
  handleBackClick() {
52
55
  if (!this.backUrl) {
53
56
  this.$emit(EVENTS.Back);
@@ -34,6 +34,7 @@
34
34
  <ActionBarNavigation
35
35
  @save="saveBrandPage"
36
36
  @publish="saveAndPublishBrandPage"
37
+ @preview="previewBrandPage"
37
38
  @create-team-cards="onCreateTeamCards"
38
39
  />
39
40
  </div>