@developer_tribe/react-builder 1.0.7 → 1.0.9

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 (217) hide show
  1. package/dist/build-components/BIcon/BIconProps.generated.d.ts +3 -0
  2. package/dist/build-components/BackgroundImage/BackgroundImageProps.generated.d.ts +1 -0
  3. package/dist/build-components/Button/ButtonProps.generated.d.ts +1 -0
  4. package/dist/build-components/Carousel/CarouselProps.generated.d.ts +5 -0
  5. package/dist/build-components/CarouselButtons/CarouselButtonsProps.generated.d.ts +1 -0
  6. package/dist/build-components/CarouselDots/CarouselDotsProps.generated.d.ts +1 -0
  7. package/dist/build-components/CarouselItem/CarouselItemProps.generated.d.ts +1 -0
  8. package/dist/build-components/CarouselProvider/CarouselProviderProps.generated.d.ts +1 -0
  9. package/dist/build-components/Image/ImageProps.generated.d.ts +1 -0
  10. package/dist/build-components/Main/MainProps.generated.d.ts +1 -1
  11. package/dist/build-components/Onboard/OnboardProps.generated.d.ts +1 -0
  12. package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +1 -0
  13. package/dist/build-components/OnboardButtons/OnboardButtonsProps.generated.d.ts +1 -0
  14. package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +1 -0
  15. package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +3 -0
  16. package/dist/build-components/OnboardImage/OnboardImageProps.generated.d.ts +1 -0
  17. package/dist/build-components/OnboardItem/OnboardItemProps.generated.d.ts +1 -0
  18. package/dist/build-components/OnboardProvider/OnboardProviderProps.generated.d.ts +3 -0
  19. package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +3 -0
  20. package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +3 -0
  21. package/dist/build-components/PaywallBackground/PaywallBackgroundProps.generated.d.ts +1 -1
  22. package/dist/build-components/PaywallCloseButton/PaywallCloseButtonProps.generated.d.ts +3 -1
  23. package/dist/build-components/PaywallOptions/PaywallOptionsProps.generated.d.ts +1 -1
  24. package/dist/build-components/PaywallProvider/PaywallContext.d.ts +12 -0
  25. package/dist/build-components/PaywallProvider/PaywallProviderProps.generated.d.ts +1 -1
  26. package/dist/build-components/PaywallSubscribeButton/PaywallSubscribeButtonProps.generated.d.ts +1 -0
  27. package/dist/build-components/RadioButton/RadioButtonProps.generated.d.ts +1 -1
  28. package/dist/build-components/Text/TextProps.generated.d.ts +3 -0
  29. package/dist/build-components/View/ViewProps.generated.d.ts +1 -0
  30. package/dist/build-components/patterns.generated.d.ts +372 -374
  31. package/dist/components/BuilderProvider.d.ts +2 -0
  32. package/dist/components/ParamsProvider.d.ts +5 -0
  33. package/dist/components/RenderErrorBoundary.d.ts +28 -0
  34. package/dist/hooks/useSyncHtmlThemeClass.d.ts +7 -0
  35. package/dist/index.cjs.js +5 -5
  36. package/dist/index.cjs.js.map +1 -1
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.esm.js +3 -3
  39. package/dist/index.esm.js.map +1 -1
  40. package/dist/index.native.cjs.js +4 -4
  41. package/dist/index.native.cjs.js.map +1 -1
  42. package/dist/index.native.d.ts +1 -0
  43. package/dist/index.native.esm.js +4 -4
  44. package/dist/index.native.esm.js.map +1 -1
  45. package/dist/migrations/migratePipe.d.ts +14 -0
  46. package/dist/migrations/migrations/1.1.0_normalize_style_attributes.d.ts +2 -0
  47. package/dist/migrations/semver.d.ts +8 -0
  48. package/dist/migrations/types.d.ts +8 -0
  49. package/dist/mockOS/components/SubscriptionModal.d.ts +7 -0
  50. package/dist/mockOS/context/MockOSContextBase.d.ts +1 -0
  51. package/dist/mockOS/hooks/useMockIap.d.ts +3 -0
  52. package/dist/mockOS/index.d.ts +4 -0
  53. package/dist/mockOS/managers/mockOSIapManager.d.ts +6 -0
  54. package/dist/mockOS/managers/subscriptionManager.d.ts +10 -0
  55. package/dist/pages/ProjectDebug.d.ts +14 -0
  56. package/dist/pages/ProjectMigrationPage.d.ts +23 -0
  57. package/dist/pages/ProjectValidationPage.d.ts +15 -0
  58. package/dist/styles.css +1 -1
  59. package/dist/types/Device.d.ts +5 -0
  60. package/dist/utils/__special_exceptions.d.ts +7 -0
  61. package/dist/utils/getImage.d.ts +23 -0
  62. package/dist/utils/pasteNode.d.ts +15 -0
  63. package/dist/utils/patterns.d.ts +1 -2
  64. package/package.json +6 -2
  65. package/scripts/migrate-patterns-to-v2.mjs +131 -0
  66. package/scripts/migrate-samples-to-current.ts +79 -0
  67. package/scripts/prebuild/utils/createGeneratedProps.js +4 -5
  68. package/scripts/prebuild/utils/validateAllComponentsOrThrow.js +32 -21
  69. package/scripts/prebuild/utils/validatePatternJson.js +12 -10
  70. package/src/.DS_Store +0 -0
  71. package/src/AttributesEditor.tsx +41 -11
  72. package/src/RenderPage.tsx +55 -0
  73. package/src/assets/.DS_Store +0 -0
  74. package/src/assets/devices.json +91 -0
  75. package/src/assets/samples/carousel-sample.json +141 -29
  76. package/src/assets/samples/getSamples.ts +9 -0
  77. package/src/assets/samples/paywall-1.json +119 -71
  78. package/src/assets/samples/simple-1.json +28 -16
  79. package/src/assets/samples/simple-2.json +157 -82
  80. package/src/assets/samples/unmigrated-builder1.json +42 -0
  81. package/src/assets/samples/unvalidated-builder1.json +49 -0
  82. package/src/assets/samples/unvalidated-crash1.json +19 -0
  83. package/src/assets/samples/unvalidated-crashcomponent1.json +16 -0
  84. package/src/assets/samples/vpn-onboard-1.json +91 -51
  85. package/src/assets/samples/vpn-onboard-2.json +318 -278
  86. package/src/assets/samples/vpn-onboard-3.json +286 -252
  87. package/src/assets/samples/vpn-onboard-4.json +286 -252
  88. package/src/assets/samples/vpn-onboard-5.json +434 -374
  89. package/src/assets/samples/vpn-onboard-6.json +290 -250
  90. package/src/attributes-editor/Field.tsx +1 -1
  91. package/src/attributes-editor/LayoutPreviewPicker.tsx +5 -2
  92. package/src/build-components/BIcon/BIconProps.generated.ts +3 -0
  93. package/src/build-components/BIcon/pattern.json +12 -9
  94. package/src/build-components/BackgroundImage/BackgroundImage.tsx +3 -1
  95. package/src/build-components/BackgroundImage/BackgroundImageProps.generated.ts +1 -0
  96. package/src/build-components/BackgroundImage/pattern.json +25 -16
  97. package/src/build-components/Button/Button.tsx +26 -3
  98. package/src/build-components/Button/ButtonProps.generated.ts +1 -0
  99. package/src/build-components/Button/pattern.json +10 -6
  100. package/src/build-components/Carousel/CarouselProps.generated.ts +5 -0
  101. package/src/build-components/Carousel/pattern.json +19 -8
  102. package/src/build-components/CarouselButtons/CarouselButtonsProps.generated.ts +1 -0
  103. package/src/build-components/CarouselButtons/pattern.json +11 -5
  104. package/src/build-components/CarouselDots/CarouselDotsProps.generated.ts +1 -0
  105. package/src/build-components/CarouselDots/pattern.json +5 -4
  106. package/src/build-components/CarouselItem/CarouselItemProps.generated.ts +1 -0
  107. package/src/build-components/CarouselItem/pattern.json +5 -4
  108. package/src/build-components/CarouselProvider/CarouselProvider.tsx +44 -2
  109. package/src/build-components/CarouselProvider/CarouselProviderProps.generated.ts +1 -0
  110. package/src/build-components/Image/Image.tsx +2 -1
  111. package/src/build-components/Image/ImageProps.generated.ts +1 -0
  112. package/src/build-components/Image/pattern.json +11 -5
  113. package/src/build-components/Main/MainProps.generated.ts +1 -1
  114. package/src/build-components/Main/pattern.json +12 -9
  115. package/src/build-components/Onboard/OnboardProps.generated.ts +1 -0
  116. package/src/build-components/Onboard/pattern.json +14 -9
  117. package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +1 -0
  118. package/src/build-components/OnboardButton/pattern.json +5 -4
  119. package/src/build-components/OnboardButtons/OnboardButtonsProps.generated.ts +1 -0
  120. package/src/build-components/OnboardButtons/pattern.json +5 -4
  121. package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +1 -0
  122. package/src/build-components/OnboardDot/pattern.json +5 -4
  123. package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +3 -0
  124. package/src/build-components/OnboardFooter/pattern.json +8 -5
  125. package/src/build-components/OnboardImage/OnboardImageProps.generated.ts +1 -0
  126. package/src/build-components/OnboardImage/pattern.json +7 -4
  127. package/src/build-components/OnboardItem/OnboardItemProps.generated.ts +1 -0
  128. package/src/build-components/OnboardItem/pattern.json +18 -9
  129. package/src/build-components/OnboardProvider/OnboardProviderProps.generated.ts +3 -0
  130. package/src/build-components/OnboardProvider/pattern.json +21 -6
  131. package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +3 -0
  132. package/src/build-components/OnboardSubtitle/pattern.json +10 -6
  133. package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +3 -0
  134. package/src/build-components/OnboardTitle/pattern.json +11 -7
  135. package/src/build-components/PaywallBackground/PaywallBackgroundProps.generated.ts +1 -1
  136. package/src/build-components/PaywallBackground/pattern.json +5 -4
  137. package/src/build-components/PaywallCloseButton/PaywallCloseButton.tsx +6 -1
  138. package/src/build-components/PaywallCloseButton/PaywallCloseButtonProps.generated.ts +3 -1
  139. package/src/build-components/PaywallCloseButton/pattern.json +15 -12
  140. package/src/build-components/PaywallOptions/PaywallOptionButton.tsx +0 -1
  141. package/src/build-components/PaywallOptions/PaywallOptions.tsx +3 -2
  142. package/src/build-components/PaywallOptions/PaywallOptionsProps.generated.ts +1 -1
  143. package/src/build-components/PaywallOptions/pattern.json +14 -11
  144. package/src/build-components/PaywallProvider/PaywallContext.ts +25 -0
  145. package/src/build-components/PaywallProvider/PaywallProvider.tsx +102 -5
  146. package/src/build-components/PaywallProvider/PaywallProviderProps.generated.ts +1 -1
  147. package/src/build-components/PaywallProvider/pattern.json +11 -8
  148. package/src/build-components/PaywallSubscribeButton/PaywallSubscribeButton.tsx +7 -0
  149. package/src/build-components/PaywallSubscribeButton/PaywallSubscribeButtonProps.generated.ts +1 -0
  150. package/src/build-components/PaywallSubscribeButton/pattern.json +16 -13
  151. package/src/build-components/RadioButton/RadioButtonProps.generated.ts +1 -1
  152. package/src/build-components/RadioButton/pattern.json +5 -4
  153. package/src/build-components/Text/Text.tsx +107 -4
  154. package/src/build-components/Text/TextProps.generated.ts +3 -0
  155. package/src/build-components/Text/pattern.json +19 -4
  156. package/src/build-components/View/ViewProps.generated.ts +1 -0
  157. package/src/build-components/View/pattern.json +28 -13
  158. package/src/build-components/other.tsx +15 -0
  159. package/src/build-components/patterns.generated.ts +340 -235
  160. package/src/build-components/useNode.ts +22 -3
  161. package/src/components/BottomBar.tsx +45 -45
  162. package/src/components/Builder.tsx +20 -6
  163. package/src/components/BuilderButton.tsx +75 -38
  164. package/src/components/BuilderProvider.tsx +22 -2
  165. package/src/components/DeviceButton.tsx +12 -5
  166. package/src/components/EditorHeader.tsx +296 -38
  167. package/src/components/ParamsProvider.tsx +7 -0
  168. package/src/components/RenderErrorBoundary.tsx +200 -0
  169. package/src/hooks/useParams.ts +5 -1
  170. package/src/hooks/useSyncHtmlThemeClass.ts +19 -0
  171. package/src/index.native.ts +7 -0
  172. package/src/index.ts +8 -0
  173. package/src/migrations/migratePipe.ts +59 -0
  174. package/src/migrations/migrations/1.1.0_normalize_style_attributes.ts +80 -0
  175. package/src/migrations/semver.ts +24 -0
  176. package/src/migrations/types.ts +9 -0
  177. package/src/mockOS/components/PermissionModal.tsx +3 -2
  178. package/src/mockOS/components/SubscriptionModal.tsx +400 -0
  179. package/src/mockOS/context/MockOSContext.tsx +61 -10
  180. package/src/mockOS/context/MockOSContextBase.ts +1 -0
  181. package/src/mockOS/hooks/useMockIap.ts +11 -0
  182. package/src/mockOS/index.ts +7 -0
  183. package/src/mockOS/managers/mockOSIapManager.ts +10 -0
  184. package/src/mockOS/managers/subscriptionManager.ts +36 -0
  185. package/src/modals/IconPickerModal.tsx +1 -1
  186. package/src/pages/ProjectDebug.tsx +331 -0
  187. package/src/pages/ProjectMigrationPage.tsx +92 -0
  188. package/src/pages/ProjectPage.tsx +318 -166
  189. package/src/pages/ProjectValidationPage.tsx +54 -0
  190. package/src/styles/base/_global.scss +58 -11
  191. package/src/styles/components/_attributes-editor.scss +1 -1
  192. package/src/styles/components/_bottom-bar.scss +7 -4
  193. package/src/styles/components/_editor-shell.scss +126 -4
  194. package/src/styles/components/_mockos-router.scss +3 -2
  195. package/src/styles/components/_ui-components.scss +10 -5
  196. package/src/styles/foundation/_colors.scss +78 -11
  197. package/src/styles/foundation/_mixins.scss +4 -1
  198. package/src/styles/foundation/_sizes.scss +4 -2
  199. package/src/styles/index.scss +1 -0
  200. package/src/styles/layout/_builder.scss +61 -0
  201. package/src/styles/layout/_project-validation.scss +214 -0
  202. package/src/styles/modals/_add-component.scss +4 -2
  203. package/src/styles/modals/_color-modal.scss +4 -2
  204. package/src/styles/modals/_modal-shell.scss +3 -1
  205. package/src/types/Device.ts +5 -0
  206. package/src/utils/__special_exceptions.ts +88 -0
  207. package/src/utils/analyseNode.ts +8 -2
  208. package/src/utils/analyseNodeByPatterns.ts +43 -9
  209. package/src/utils/extractTextStyle.ts +19 -6
  210. package/src/utils/extractViewStyle.ts +68 -59
  211. package/src/utils/getImage.ts +76 -0
  212. package/src/utils/novaToJson.ts +2 -1
  213. package/src/utils/pasteNode.ts +172 -0
  214. package/src/utils/patterns.ts +4 -3
  215. package/dist/android.svg +0 -43
  216. package/dist/apple.svg +0 -16
  217. package/dist/background.jpg +0 -0
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useState } from 'react';
2
2
  import type { Node } from '../types/Node';
3
3
  import type { Project, ProjectColors } from '../types/Project';
4
+ import { ToastContainer, toast } from 'react-toastify';
4
5
  import { RenderPage } from '../RenderPage';
5
6
  import { EditorHeader } from '../components/EditorHeader';
6
7
  import { AttributesEditorPanel } from '../components/AttributesEditorPanel';
@@ -12,10 +13,21 @@ import { useRenderStore } from '../store';
12
13
  import { logger } from '../utils/logger';
13
14
  import { useLogRender } from '../utils/useLogRender';
14
15
  import type { LogLevel } from '../types/Project';
15
- import { analyseAndProccess } from '../utils/analyseNode';
16
- import backgroundImage from '../assets/images/background.jpg';
16
+ import {
17
+ analyseAndProccess,
18
+ isEmptyObject,
19
+ isNodeNullOrUndefined,
20
+ } from '../utils/analyseNode';
21
+ import { getImage, TribeAssetName } from '../utils/getImage';
17
22
  import type { PaywallBenefits } from '../paywall/types/benefits';
18
23
  import { LoadingComponent } from '../components/LoadingComponent';
24
+ import { ProjectValidationPage } from './ProjectValidationPage';
25
+ import { ProjectMigrationPage } from './ProjectMigrationPage';
26
+ import { useSyncHtmlThemeClass } from '../hooks/useSyncHtmlThemeClass';
27
+ import {
28
+ getMigrationPipe,
29
+ runProjectMigrations,
30
+ } from '../migrations/migratePipe';
19
31
  import {
20
32
  deleteNodeFromTree,
21
33
  findNodeByKey,
@@ -44,8 +56,11 @@ export function ProjectPage({
44
56
  name,
45
57
  }: ProjectPageProps) {
46
58
  useLogRender('ProjectPage');
59
+ useSyncHtmlThemeClass();
47
60
  const resolvedName = name ?? project.name;
48
61
  const resolvedProjectColors = projectColors ?? project.projectColors;
62
+ const isEmptyProjectData =
63
+ isNodeNullOrUndefined(project.data) || isEmptyObject(project.data);
49
64
  const {
50
65
  current,
51
66
  setCurrent,
@@ -53,7 +68,7 @@ export function ProjectPage({
53
68
  setProjectName,
54
69
  products,
55
70
  benefits,
56
- } = useRenderStore(s => ({
71
+ } = useRenderStore((s) => ({
57
72
  current: s.current,
58
73
  setCurrent: s.setCurrent,
59
74
  setProjectColors: s.setProjectColors,
@@ -61,9 +76,23 @@ export function ProjectPage({
61
76
  products: s.products,
62
77
  benefits: s.benefits,
63
78
  }));
64
- const [editorData, setEditorData] = useState<Node>(null);
65
- const [minLoadingDelayDone, setMinLoadingDelayDone] =
66
- useState<boolean>(false);
79
+ const [editorData, setEditorData] = useState<Node>(() => {
80
+ if (!isEmptyProjectData) return null;
81
+ // Empty project should start in a usable state (no loader / no error).
82
+ return analyseAndProccess({ type: 'Main', children: [] }) as Node;
83
+ });
84
+ const [validationError, setValidationError] = useState<string | null>(null);
85
+ const [validationErrorStack, setValidationErrorStack] = useState<
86
+ string | null
87
+ >(null);
88
+ const [bypassValidation, setBypassValidation] = useState<boolean>(false);
89
+ const [migrationGate, setMigrationGate] = useState<ReturnType<
90
+ typeof getMigrationPipe
91
+ > | null>(null);
92
+ const [isMigrating, setIsMigrating] = useState<boolean>(false);
93
+ const [minLoadingDelayDone, setMinLoadingDelayDone] = useState<boolean>(
94
+ () => isEmptyProjectData,
95
+ );
67
96
  const [mobilePanel, setMobilePanel] = useState<
68
97
  'builder' | 'attributes' | null
69
98
  >(null);
@@ -77,7 +106,7 @@ export function ProjectPage({
77
106
  // Extra warning for deleting the root node (editorData)
78
107
  if (nodeToDelete === editorData) {
79
108
  const shouldDeleteRoot = window.confirm(
80
- 'You are about to delete the root component. This will clear the entire screen. Continue?'
109
+ 'You are about to delete the root component. This will clear the entire screen. Continue?',
81
110
  );
82
111
  if (!shouldDeleteRoot) return;
83
112
  setEditorData(null);
@@ -103,7 +132,7 @@ export function ProjectPage({
103
132
  }
104
133
  }
105
134
  },
106
- [editorData, current]
135
+ [editorData, current],
107
136
  );
108
137
 
109
138
  useEffect(() => {
@@ -144,7 +173,7 @@ export function ProjectPage({
144
173
  }, [isMobile]);
145
174
 
146
175
  const toggleMobilePanel = (panel: 'builder' | 'attributes') => {
147
- setMobilePanel(prev => (prev === panel ? null : panel));
176
+ setMobilePanel((prev) => (prev === panel ? null : panel));
148
177
  };
149
178
 
150
179
  const closeMobilePanels = () => {
@@ -155,6 +184,11 @@ export function ProjectPage({
155
184
  const attributesPanelIsOpen = !isMobile || mobilePanel === 'attributes';
156
185
 
157
186
  useEffect(() => {
187
+ if (isEmptyProjectData) {
188
+ // Don't show loading for an empty project.
189
+ setMinLoadingDelayDone(true);
190
+ return;
191
+ }
158
192
  setMinLoadingDelayDone(false);
159
193
  const timer = setTimeout(() => setMinLoadingDelayDone(true), 1000);
160
194
  return () => clearTimeout(timer);
@@ -164,40 +198,87 @@ export function ProjectPage({
164
198
  try {
165
199
  // Reset to "loading" immediately on project change so the loader is shown
166
200
  // until a valid node is available (and for at least 2 seconds).
167
- setEditorData(null);
168
- setCurrent(null);
169
- const processed = analyseAndProccess(project.data);
170
- if (!processed) {
201
+ if (!isEmptyProjectData) {
202
+ setEditorData(null);
203
+ setCurrent(null);
204
+ }
205
+ setValidationError(null);
206
+ setValidationErrorStack(null);
207
+ // Version gate: if project is older than the current schema, show migration UI.
208
+ const pipe = getMigrationPipe(project);
209
+ if (!bypassValidation && pipe.required) {
210
+ setMigrationGate(pipe);
171
211
  setEditorData(null);
172
212
  setCurrent(null);
173
213
  return;
174
214
  }
215
+ setMigrationGate(null);
216
+ if (bypassValidation) {
217
+ // Best-effort: let the user continue with the raw data even if invalid.
218
+ // This may still crash the preview, but it unblocks users for debugging.
219
+ setEditorData(project.data as unknown as Node);
220
+ setCurrent(project.data as unknown as Node);
221
+ return;
222
+ }
223
+ const inputNode: Node = isEmptyProjectData
224
+ ? { type: 'Main', children: [] }
225
+ : (project.data as Node);
226
+
227
+ const processed = analyseAndProccess(inputNode);
228
+ if (!processed) return;
175
229
  setEditorData(processed);
176
230
  setCurrent(processed);
177
231
  } catch (error) {
178
232
  console.error(error);
233
+ setValidationError(
234
+ error instanceof Error ? error.message : 'Node is not valid',
235
+ );
236
+ setValidationErrorStack(
237
+ error instanceof Error ? (error.stack ?? null) : null,
238
+ );
179
239
  setEditorData(null);
180
240
  setCurrent(null);
181
241
  }
182
- }, [project.data]);
242
+ }, [project, project.data, bypassValidation, setCurrent]);
183
243
 
184
- const showLoading = editorData === null || !minLoadingDelayDone;
244
+ const showLoading =
245
+ !isEmptyProjectData && (editorData === null || !minLoadingDelayDone);
185
246
 
186
247
  return (
187
- <div className='container-full'>
248
+ <div className="container-full">
249
+ <ToastContainer
250
+ position="bottom-right"
251
+ autoClose={2500}
252
+ hideProgressBar={false}
253
+ newestOnTop
254
+ closeOnClick
255
+ pauseOnFocusLoss
256
+ draggable
257
+ pauseOnHover
258
+ theme="colored"
259
+ />
188
260
  <EditorHeader
189
261
  onSaveProject={() => {
190
- logger.info('ProjectPage', 'save project', { name: project.name });
191
- if (onSaveProjectColors && resolvedProjectColors) {
192
- onSaveProjectColors(resolvedProjectColors);
262
+ try {
263
+ logger.info('ProjectPage', 'save project', { name: project.name });
264
+ if (onSaveProjectColors && resolvedProjectColors) {
265
+ onSaveProjectColors(resolvedProjectColors);
266
+ }
267
+ onSaveProject({
268
+ ...project,
269
+ data: editorData,
270
+ });
271
+ toast.success('Saved');
272
+ } catch (e) {
273
+ logger.error('ProjectPage', 'save project failed', e);
274
+ toast.error('Save failed');
193
275
  }
194
- onSaveProject({
195
- ...project,
196
- data: editorData,
197
- });
198
276
  }}
199
277
  onRestoreProject={() => {
200
278
  logger.info('ProjectPage', 'restore project', { name: project.name });
279
+ setValidationError(null);
280
+ setValidationErrorStack(null);
281
+ setBypassValidation(false);
201
282
  setEditorData(project.data);
202
283
  setCurrent(project.data);
203
284
  }}
@@ -205,156 +286,227 @@ export function ProjectPage({
205
286
  editorData={editorData}
206
287
  setEditorData={setEditorData}
207
288
  />
208
- {isMobile && (
209
- <div
210
- className='mobile-panel-toggle'
211
- role='group'
212
- aria-label='Editor panels'
213
- >
214
- <button
215
- type='button'
216
- className={`mobile-panel-toggle__button${mobilePanel === 'builder' ? ' mobile-panel-toggle__button--active' : ''}`}
217
- aria-label='Toggle builder panel'
218
- aria-expanded={mobilePanel === 'builder'}
219
- aria-controls='split-left-panel'
220
- onClick={() => toggleMobilePanel('builder')}
221
- >
222
- <span className='mobile-panel-toggle__icon' aria-hidden='true'>
223
- <svg viewBox='0 0 16 12' role='presentation' focusable='false'>
224
- <path
225
- d='M1 1h14M1 6h14M1 11h14'
226
- stroke='currentColor'
227
- strokeWidth='2'
228
- strokeLinecap='round'
229
- fill='none'
230
- />
231
- </svg>
232
- </span>
233
- <span className='mobile-panel-toggle__label'>Builder</span>
234
- </button>
235
- <button
236
- type='button'
237
- className={`mobile-panel-toggle__button${mobilePanel === 'attributes' ? ' mobile-panel-toggle__button--active' : ''}`}
238
- aria-label='Toggle attributes panel'
239
- aria-expanded={mobilePanel === 'attributes'}
240
- aria-controls='split-attributes-panel'
241
- onClick={() => toggleMobilePanel('attributes')}
242
- >
243
- <span className='mobile-panel-toggle__icon' aria-hidden='true'>
244
- <svg viewBox='0 0 16 12' role='presentation' focusable='false'>
245
- <path
246
- d='M1 1h14M1 6h14M1 11h14'
247
- stroke='currentColor'
248
- strokeWidth='2'
249
- strokeLinecap='round'
250
- fill='none'
251
- />
252
- </svg>
253
- </span>
254
- <span className='mobile-panel-toggle__label'>Attributes</span>
255
- </button>
256
- </div>
257
- )}
258
- <div className='editor-container'>
259
- <div
260
- id='split-left-panel'
261
- className={`split-left${leftPanelIsOpen ? ' is-open' : ''}`}
262
- aria-hidden={isMobile && !leftPanelIsOpen}
263
- >
289
+ {migrationGate ? (
290
+ <ProjectMigrationPage
291
+ name={resolvedName}
292
+ rawData={project.data}
293
+ projectVersion={migrationGate.projectVersion}
294
+ requiredVersion={migrationGate.requiredVersion}
295
+ pendingMigrations={migrationGate.pending.map((m) => ({
296
+ id: m.id,
297
+ title: m.title,
298
+ fromVersion: m.fromVersion,
299
+ toVersion: m.toVersion,
300
+ }))}
301
+ products={products}
302
+ benefits={benefits}
303
+ canvasBg={`url(${getImage(TribeAssetName.Background)})`}
304
+ migrating={isMigrating}
305
+ onContinueWithoutValidation={() => {
306
+ setBypassValidation(true);
307
+ setMigrationGate(null);
308
+ setEditorData(project.data as unknown as Node);
309
+ setCurrent(project.data as unknown as Node);
310
+ setMinLoadingDelayDone(true);
311
+ }}
312
+ onMigrateNow={() => {
313
+ try {
314
+ setIsMigrating(true);
315
+ const { project: migratedProject } =
316
+ runProjectMigrations(project);
317
+ onSaveProject(migratedProject);
318
+ setBypassValidation(true);
319
+ setMigrationGate(null);
320
+ setEditorData(migratedProject.data as unknown as Node);
321
+ setCurrent(migratedProject.data as unknown as Node);
322
+ setMinLoadingDelayDone(true);
323
+ } finally {
324
+ setIsMigrating(false);
325
+ }
326
+ }}
327
+ />
328
+ ) : validationError ? (
329
+ <ProjectValidationPage
330
+ name={resolvedName}
331
+ rawData={project.data}
332
+ validationError={validationError}
333
+ validationErrorStack={validationErrorStack ?? undefined}
334
+ products={products}
335
+ benefits={benefits}
336
+ canvasBg={`url(${getImage(TribeAssetName.Background)})`}
337
+ onContinueWithoutValidation={() => {
338
+ setBypassValidation(true);
339
+ setValidationError(null);
340
+ setValidationErrorStack(null);
341
+ setEditorData(project.data as unknown as Node);
342
+ setCurrent(project.data as unknown as Node);
343
+ setMinLoadingDelayDone(true);
344
+ }}
345
+ />
346
+ ) : (
347
+ <>
264
348
  {isMobile && (
265
- <button
266
- type='button'
267
- className='split-panel__close'
268
- aria-label='Close builder panel'
269
- onClick={closeMobilePanels}
349
+ <div
350
+ className="mobile-panel-toggle"
351
+ role="group"
352
+ aria-label="Editor panels"
270
353
  >
271
- Close
272
- </button>
273
- )}
274
- <div>
275
- <BuilderPanel
276
- data={editorData}
277
- setData={setEditorData}
278
- onDeleteNode={handleDeleteNode}
279
- />
280
- </div>
281
- </div>
282
- <div
283
- style={{
284
- // Set as a CSS variable so `.dark .split-right` can override it.
285
- // eslint-disable-next-line @typescript-eslint/naming-convention
286
- ['--rb-canvas-bg' as any]: `url(${backgroundImage})`,
287
- }}
288
- className='split-right'
289
- >
290
- {showLoading && (
291
- <div className='rb-loading-overlay' aria-busy='true'>
292
- <LoadingComponent />
354
+ <button
355
+ type="button"
356
+ className={`mobile-panel-toggle__button${mobilePanel === 'builder' ? ' mobile-panel-toggle__button--active' : ''}`}
357
+ aria-label="Toggle builder panel"
358
+ aria-expanded={mobilePanel === 'builder'}
359
+ aria-controls="split-left-panel"
360
+ onClick={() => toggleMobilePanel('builder')}
361
+ >
362
+ <span className="mobile-panel-toggle__icon" aria-hidden="true">
363
+ <svg
364
+ viewBox="0 0 16 12"
365
+ role="presentation"
366
+ focusable="false"
367
+ >
368
+ <path
369
+ d="M1 1h14M1 6h14M1 11h14"
370
+ stroke="currentColor"
371
+ strokeWidth="2"
372
+ strokeLinecap="round"
373
+ fill="none"
374
+ />
375
+ </svg>
376
+ </span>
377
+ <span className="mobile-panel-toggle__label">Builder</span>
378
+ </button>
379
+ <button
380
+ type="button"
381
+ className={`mobile-panel-toggle__button${mobilePanel === 'attributes' ? ' mobile-panel-toggle__button--active' : ''}`}
382
+ aria-label="Toggle attributes panel"
383
+ aria-expanded={mobilePanel === 'attributes'}
384
+ aria-controls="split-attributes-panel"
385
+ onClick={() => toggleMobilePanel('attributes')}
386
+ >
387
+ <span className="mobile-panel-toggle__icon" aria-hidden="true">
388
+ <svg
389
+ viewBox="0 0 16 12"
390
+ role="presentation"
391
+ focusable="false"
392
+ >
393
+ <path
394
+ d="M1 1h14M1 6h14M1 11h14"
395
+ stroke="currentColor"
396
+ strokeWidth="2"
397
+ strokeLinecap="round"
398
+ fill="none"
399
+ />
400
+ </svg>
401
+ </span>
402
+ <span className="mobile-panel-toggle__label">Attributes</span>
403
+ </button>
293
404
  </div>
294
405
  )}
295
- {/* NOTE: In React Native, `products` should come from an IAP wrapper (e.g. `react-native-iap`). */}
296
- {!showLoading && editorData && (
297
- <BuilderProvider
298
- params={{
299
- products,
300
- benefits:
301
- benefits && typeof benefits === 'object'
302
- ? (benefits as PaywallBenefits)
303
- : {},
406
+ <div className="editor-container">
407
+ <div
408
+ id="split-left-panel"
409
+ className={`split-left${leftPanelIsOpen ? ' is-open' : ''}`}
410
+ aria-hidden={isMobile && !leftPanelIsOpen}
411
+ >
412
+ {isMobile && (
413
+ <button
414
+ type="button"
415
+ className="split-panel__close"
416
+ aria-label="Close builder panel"
417
+ onClick={closeMobilePanels}
418
+ >
419
+ Close
420
+ </button>
421
+ )}
422
+ <div>
423
+ <BuilderPanel
424
+ data={editorData}
425
+ setData={setEditorData}
426
+ onDeleteNode={handleDeleteNode}
427
+ />
428
+ </div>
429
+ </div>
430
+ <div
431
+ style={{
432
+ // Set as a CSS variable so `.dark .split-right` can override it.
433
+ // eslint-disable-next-line @typescript-eslint/naming-convention
434
+ ['--rb-canvas-bg' as any]: `url(${getImage(
435
+ TribeAssetName.Background,
436
+ )})`,
304
437
  }}
438
+ className="split-right"
305
439
  >
306
- <RenderPage data={editorData} name={resolvedName} />
307
- </BuilderProvider>
308
- )}
309
- </div>
310
- {/* BOTOM BAR */}
311
- <BottomBar data={editorData} setData={setEditorData} />
312
- <div
313
- id='split-attributes-panel'
314
- className={`split-third${attributesPanelIsOpen ? ' is-open' : ''}`}
315
- aria-hidden={isMobile && !attributesPanelIsOpen}
316
- >
317
- {isMobile && (
318
- <button
319
- type='button'
320
- className='split-panel__close'
321
- aria-label='Close attributes panel'
322
- onClick={closeMobilePanels}
440
+ {showLoading && (
441
+ <div className="rb-loading-overlay" aria-busy="true">
442
+ <LoadingComponent />
443
+ </div>
444
+ )}
445
+ {/* NOTE: In React Native, `products` should come from an IAP wrapper (e.g. `react-native-iap`). */}
446
+ {!showLoading && editorData && (
447
+ <BuilderProvider
448
+ params={{
449
+ products,
450
+ benefits:
451
+ benefits && typeof benefits === 'object'
452
+ ? (benefits as PaywallBenefits)
453
+ : {},
454
+ }}
455
+ >
456
+ <RenderPage data={editorData} name={resolvedName} />
457
+ </BuilderProvider>
458
+ )}
459
+ </div>
460
+ {/* BOTOM BAR */}
461
+ <BottomBar data={editorData} setData={setEditorData} />
462
+ <div
463
+ id="split-attributes-panel"
464
+ className={`split-third${attributesPanelIsOpen ? ' is-open' : ''}`}
465
+ aria-hidden={isMobile && !attributesPanelIsOpen}
323
466
  >
324
- Close
325
- </button>
326
- )}
327
- <AttributesEditorPanel
328
- attributes={editorData}
329
- projectColors={resolvedProjectColors}
330
- onChange={data => {
331
- setEditorData(data);
332
- let nodeKey: string | undefined = undefined;
333
- if (
334
- data &&
335
- typeof data === 'object' &&
336
- !Array.isArray(data) &&
337
- 'key' in (data as any)
338
- ) {
339
- nodeKey = (data as any).key as string | undefined;
340
- }
341
- logger.verbose(
342
- 'ProjectPage',
343
- 'attributes change',
344
- nodeKey ? { nodeKey } : undefined
345
- );
346
- }}
347
- />
348
- </div>
349
- {isMobile && mobilePanel && (
350
- <button
351
- type='button'
352
- className='editor-container__overlay'
353
- aria-label='Close active panel'
354
- onClick={closeMobilePanels}
355
- />
356
- )}
357
- </div>
467
+ {isMobile && (
468
+ <button
469
+ type="button"
470
+ className="split-panel__close"
471
+ aria-label="Close attributes panel"
472
+ onClick={closeMobilePanels}
473
+ >
474
+ Close
475
+ </button>
476
+ )}
477
+ <AttributesEditorPanel
478
+ attributes={editorData}
479
+ projectColors={resolvedProjectColors}
480
+ onChange={(data) => {
481
+ setEditorData(data);
482
+ let nodeKey: string | undefined = undefined;
483
+ if (
484
+ data &&
485
+ typeof data === 'object' &&
486
+ !Array.isArray(data) &&
487
+ 'key' in (data as any)
488
+ ) {
489
+ nodeKey = (data as any).key as string | undefined;
490
+ }
491
+ logger.verbose(
492
+ 'ProjectPage',
493
+ 'attributes change',
494
+ nodeKey ? { nodeKey } : undefined,
495
+ );
496
+ }}
497
+ />
498
+ </div>
499
+ {isMobile && mobilePanel && (
500
+ <button
501
+ type="button"
502
+ className="editor-container__overlay"
503
+ aria-label="Close active panel"
504
+ onClick={closeMobilePanels}
505
+ />
506
+ )}
507
+ </div>
508
+ </>
509
+ )}
358
510
  </div>
359
511
  );
360
512
  }
@@ -0,0 +1,54 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { Product } from '../paywall/types/paywall-types';
3
+ import type { PaywallBenefits } from '../paywall/types/benefits';
4
+ import { ProjectDebug } from './ProjectDebug';
5
+
6
+ export type ProjectValidationPageProps = {
7
+ name: string;
8
+ rawData: unknown;
9
+ validationError: string;
10
+ validationErrorStack?: string;
11
+ products: Product[];
12
+ benefits: PaywallBenefits;
13
+ canvasBg?: string;
14
+ belowName?: ReactNode;
15
+ onContinueWithoutValidation?: () => void;
16
+ };
17
+
18
+ export function ProjectValidationPage({
19
+ name,
20
+ rawData,
21
+ validationError,
22
+ validationErrorStack,
23
+ products,
24
+ benefits,
25
+ canvasBg,
26
+ belowName,
27
+ onContinueWithoutValidation,
28
+ }: ProjectValidationPageProps) {
29
+ return (
30
+ <ProjectDebug
31
+ name={name}
32
+ rawData={rawData}
33
+ validationError={validationError}
34
+ validationErrorStack={validationErrorStack}
35
+ products={products}
36
+ benefits={benefits}
37
+ canvasBg={canvasBg}
38
+ belowName={
39
+ <>
40
+ <div className="rb-project-validation__actions">
41
+ <button
42
+ type="button"
43
+ className="editor-button"
44
+ onClick={() => onContinueWithoutValidation?.()}
45
+ >
46
+ Continue without validation
47
+ </button>
48
+ </div>
49
+ {belowName}
50
+ </>
51
+ }
52
+ />
53
+ );
54
+ }