@developer_tribe/react-builder 1.2.15 → 1.2.17

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 (46) hide show
  1. package/dist/build-components/PaywallCloseButton/PaywallCloseButton.d.ts +1 -1
  2. package/dist/build-components/PaywallProvider/PaywallContext.d.ts +2 -1
  3. package/dist/build-components/PaywallProvider/PaywallProviderProps.generated.d.ts +1 -0
  4. package/dist/build-components/patterns.generated.d.ts +11 -1
  5. package/dist/index.cjs.js +3 -3
  6. package/dist/index.cjs.js.map +1 -1
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.esm.js +4 -4
  9. package/dist/index.esm.js.map +1 -1
  10. package/dist/index.web.cjs.js +3 -3
  11. package/dist/index.web.cjs.js.map +1 -1
  12. package/dist/index.web.esm.js +3 -3
  13. package/dist/index.web.esm.js.map +1 -1
  14. package/dist/mockOS/backHandler.d.ts +4 -0
  15. package/dist/paywall/hooks/index.d.ts +3 -3
  16. package/dist/paywall/hooks/useChangeDelayByPaywall.d.ts +4 -0
  17. package/dist/paywall/hooks/useHandleGoBack.d.ts +1 -0
  18. package/dist/paywall/hooks/useMockOSBackHandler.d.ts +1 -0
  19. package/package.json +4 -1
  20. package/src/assets/meta.json +1 -1
  21. package/src/assets/samples/paywall-2.json +3 -2
  22. package/src/build-components/CountDown/CountDown.tsx +9 -1
  23. package/src/build-components/PaywallCloseButton/PaywallCloseButton.tsx +5 -1
  24. package/src/build-components/PaywallCloseButton/pattern.json +3 -0
  25. package/src/build-components/PaywallCounter/PaywallCounter.tsx +2 -5
  26. package/src/build-components/PaywallOptions/PaywallOptions.tsx +2 -2
  27. package/src/build-components/PaywallProvider/PaywallContext.ts +4 -2
  28. package/src/build-components/PaywallProvider/PaywallProvider.tsx +45 -7
  29. package/src/build-components/PaywallProvider/PaywallProviderProps.generated.ts +1 -0
  30. package/src/build-components/PaywallProvider/pattern.json +11 -1
  31. package/src/build-components/patterns.generated.ts +12 -1
  32. package/src/components/DeviceNavigationBar.tsx +5 -0
  33. package/src/index.ts +1 -3
  34. package/src/mockOS/backHandler.ts +35 -0
  35. package/src/pages/ProjectMigrationPage.tsx +10 -1
  36. package/src/pages/ProjectPage.tsx +7 -0
  37. package/src/paywall/hooks/index.ts +3 -3
  38. package/src/paywall/hooks/useChangeDelayByPaywall.ts +25 -0
  39. package/src/paywall/hooks/useHandleGoBack.ts +60 -0
  40. package/src/paywall/hooks/useMockOSBackHandler.ts +9 -0
  41. package/dist/paywall/hooks/useCarouselOptionsSeperator.d.ts +0 -6
  42. package/dist/paywall/hooks/useCloseStatusPaywall.d.ts +0 -4
  43. package/dist/paywall/hooks/usePaywallCounter.d.ts +0 -4
  44. package/src/paywall/hooks/useCarouselOptionsSeperator.ts +0 -8
  45. package/src/paywall/hooks/useCloseStatusPaywall.ts +0 -6
  46. package/src/paywall/hooks/usePaywallCounter.ts +0 -6
@@ -0,0 +1,4 @@
1
+ export type MockOSBackHandler = () => boolean;
2
+ export declare function registerMockOSBackHandler(handler: MockOSBackHandler): () => void;
3
+ export declare function emitMockOSBackPress(): boolean;
4
+ export declare function clearMockOSBackHandlers(): void;
@@ -1,5 +1,5 @@
1
- export { usePaywallCounter } from './usePaywallCounter';
2
- export { useCloseStatusPaywall } from './useCloseStatusPaywall';
3
- export { useCarouselOptionsSeperator } from './useCarouselOptionsSeperator';
4
1
  export { useCalculateLocalizedPrice } from './useCalculateLocalizedPrice';
5
2
  export { useDiscountRate } from './useDiscountRate';
3
+ export { useChangeDelayByPaywall } from './useChangeDelayByPaywall';
4
+ export { useHandleGoBack } from './useHandleGoBack';
5
+ export { useMockOSBackHandler } from './useMockOSBackHandler';
@@ -0,0 +1,4 @@
1
+ import type { NodeData } from '../../types/Node';
2
+ export declare function useChangeDelayByPaywall(node: NodeData<{
3
+ delay?: number;
4
+ }> | null | undefined, setIsBackAllowed: (value: boolean) => void): void;
@@ -0,0 +1 @@
1
+ export declare function useHandleGoBack(handleGoBack: () => boolean | null | undefined): void;
@@ -0,0 +1 @@
1
+ export declare function useMockOSBackHandler(isBackAllowed: boolean): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@developer_tribe/react-builder",
3
- "version": "1.2.15",
3
+ "version": "1.2.17",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "restricted": true,
@@ -33,6 +33,9 @@
33
33
  ],
34
34
  "scripts": {
35
35
  "prebuild": "node ./scripts/prebuild/prebuild.js",
36
+ "prebuild:basics": "yarn builder build --name builder --components=View --components=Text --components=Button --components=Image --components=Carousel --components=CarouselProvider --components=CarouselItem --components=CarouselDots --components=CarouselButtons --components=BIcon --components=BackgroundImage --components=Main --components=RadioButton --components=Counter --components=CountDown",
37
+ "prebuild:onboard": "yarn builder build --name onboard/builder --components=OnboardProvider --components=Onboard --components=OnboardItem --components=OnboardImage --components=OnboardTitle --components=OnboardSubtitle --components=OnboardDot --components=OnboardButtons --components=OnboardButton --components=OnboardFooter",
38
+ "prebuild:paywall": "yarn builder build --name paywall/builder --components=PaywallProvider --components=PaywallBackground --components=PaywallCloseButton --components=PaywallOptions --components=PaywallSubscribeButton --components=PaywallCounter",
36
39
  "build": "yarn prebuild && yarn rimraf dist && rollup -c",
37
40
  "build:watch": "rollup -c -w",
38
41
  "lint": "eslint src",
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "supportedProjectVersion": "1.1.2",
3
- "reactBuilderVersion": "1.2.15"
3
+ "reactBuilderVersion": "1.2.17"
4
4
  }
@@ -37,6 +37,7 @@
37
37
  "backgroundColor": "#EEF2FF"
38
38
  },
39
39
  "scrollable": true,
40
+ "delay": 5000,
40
41
  "description": "Paywall provider container.",
41
42
  "title": "Main Paywall"
42
43
  },
@@ -293,14 +294,14 @@
293
294
  {
294
295
  "type": "CountDown",
295
296
  "attributes": {
296
- "count": 30000,
297
+ "count": 10000,
297
298
  "style": {
298
299
  "color": "#FF0000",
299
300
  "fontWeight": "700",
300
301
  "fontSize": 40,
301
302
  "textAlign": "center"
302
303
  },
303
- "description": "Countdown timer (30 seconds).",
304
+ "description": "Countdown timer (10 seconds).",
304
305
  "title": "Countdown"
305
306
  },
306
307
  "children": null
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useId, useState } from 'react';
1
+ import React, { useEffect, useId, useRef, useState } from 'react';
2
2
  import type { CountDownComponentProps } from './CountDownProps.generated';
3
3
  import useNode from '../useNode';
4
4
  import { useBuilderParams } from '../../components/BuilderProvider';
@@ -7,6 +7,7 @@ import { useLogRender } from '../../utils/useLogRender';
7
7
  import { isNodeSelected, SELECTED_OUTLINE_STYLE } from '../../utils/selection';
8
8
  import { useMergedStyle } from '../../utils/useMergedStyle';
9
9
  import { formatCountdownTime } from './formatCountdownTime';
10
+ import { usePaywallContext } from '../PaywallProvider/PaywallContext';
10
11
 
11
12
  const DEFAULT_FORMAT = 'm : s';
12
13
 
@@ -20,6 +21,8 @@ export function CountDown({ node }: CountDownComponentProps) {
20
21
  const attributeKey = node.key ?? generatedId;
21
22
 
22
23
  const { previewMode, selectedKey } = useBuilderParams();
24
+ const { onCounterDown } = usePaywallContext();
25
+ const onCounterDownRef = useRef(onCounterDown);
23
26
  const baseStyle = useExtractTextStyle(node);
24
27
  const isSelected = isNodeSelected({
25
28
  previewMode: !!previewMode,
@@ -39,6 +42,10 @@ export function CountDown({ node }: CountDownComponentProps) {
39
42
 
40
43
  const [time, setTime] = useState<string>('');
41
44
 
45
+ useEffect(() => {
46
+ onCounterDownRef.current = onCounterDown;
47
+ }, [onCounterDown]);
48
+
42
49
  useEffect(() => {
43
50
  const targetTimeMs = Date.now() + delayMs;
44
51
  setTime(formatCountdownTime(targetTimeMs, DEFAULT_FORMAT));
@@ -53,6 +60,7 @@ export function CountDown({ node }: CountDownComponentProps) {
53
60
  setTime(formatCountdownTime(targetTimeMs, DEFAULT_FORMAT, nowMs));
54
61
  if (diffMs <= 0) {
55
62
  clearInterval(intervalId);
63
+ onCounterDownRef.current?.();
56
64
  }
57
65
  }, 950); // Keep slightly under 1s to reduce visible drift.
58
66
 
@@ -14,7 +14,7 @@ import { usePaywallContext } from '../PaywallProvider/PaywallContext';
14
14
  function PaywallCloseButton({ node }: PaywallCloseButtonComponentProps) {
15
15
  useLogRender('PaywallCloseButton');
16
16
  node = useNode(node);
17
- const { onClose } = usePaywallContext();
17
+ const { onClose, isBackAllowed } = usePaywallContext();
18
18
 
19
19
  const generatedId = useId();
20
20
  const attributeName = node.sourceType ?? node.type ?? 'PaywallCloseButton';
@@ -35,6 +35,10 @@ function PaywallCloseButton({ node }: PaywallCloseButtonComponentProps) {
35
35
  isSelected ? SELECTED_OUTLINE_STYLE : undefined,
36
36
  );
37
37
 
38
+ if (!isBackAllowed) {
39
+ return null;
40
+ }
41
+
38
42
  const attrs = node.attributes;
39
43
  const styleBag = attrs?.style;
40
44
  const iconType = attrs?.iconType ?? 'close';
@@ -21,6 +21,9 @@
21
21
  "iconType": "close",
22
22
  "size": 24,
23
23
  "style": {
24
+ "position": "absolute",
25
+ "top": "35@vs",
26
+ "left": "24@s",
24
27
  "flexDirection": "row",
25
28
  "justifyContent": "center",
26
29
  "alignItems": "center"
@@ -6,7 +6,6 @@ import { useExtractTextStyle } from '../../attribute-analyser/style/web/useExtra
6
6
  import { useLogRender } from '../../utils/useLogRender';
7
7
  import { isNodeSelected, SELECTED_OUTLINE_STYLE } from '../../utils/selection';
8
8
  import { useMergedStyle } from '../../utils/useMergedStyle';
9
- import { usePaywallCounter } from '../../paywall/hooks';
10
9
 
11
10
  //Optimzation trade off by readability: skip React.memo to keep named exports.
12
11
  export function PaywallCounter({ node }: PaywallCounterComponentProps) {
@@ -29,11 +28,9 @@ export function PaywallCounter({ node }: PaywallCounterComponentProps) {
29
28
  isSelected ? SELECTED_OUTLINE_STYLE : undefined,
30
29
  );
31
30
 
32
- const paywallCount = usePaywallCounter();
33
31
  const fallbackCount = node.attributes?.count;
34
- const count = Number.isFinite(paywallCount)
35
- ? paywallCount
36
- : typeof fallbackCount === 'number' && Number.isFinite(fallbackCount)
32
+ const count =
33
+ typeof fallbackCount === 'number' && Number.isFinite(fallbackCount)
37
34
  ? fallbackCount
38
35
  : 0;
39
36
 
@@ -52,7 +52,7 @@ function PaywallOptions({ node }: PaywallOptionsComponentProps) {
52
52
  useLogRender('PaywallOptions');
53
53
  node = useNode(node);
54
54
  const { products } = useBuilderParams();
55
- const { selectedProductId, setSelectedProductId } = usePaywallContext();
55
+ const { selectedProduct, setSelectedProductId } = usePaywallContext();
56
56
  const getParamsForProduct = usePaywallOptionParamsFactory();
57
57
 
58
58
  const sortedProducts = useMemo(() => {
@@ -67,7 +67,7 @@ function PaywallOptions({ node }: PaywallOptionsComponentProps) {
67
67
  <>
68
68
  {sortedProducts.map((p, index) => {
69
69
  const productId = p.productId || `${index}`;
70
- const isSelected = selectedProductId === productId;
70
+ const isSelected = selectedProduct?.productId === productId;
71
71
  const { localizationParams, otherParams } = getParamsForProduct(p, {
72
72
  isSelected,
73
73
  });
@@ -3,11 +3,12 @@ import type { Product } from '../../paywall/types/paywall-types';
3
3
 
4
4
  export type PaywallContextValue = {
5
5
  products: Product[];
6
- selectedProductId: string;
7
6
  setSelectedProductId: (productId: string) => void;
8
7
  selectedProduct?: Product;
9
8
  onClose?: () => void;
10
9
  onSubscribe?: (product?: Product) => void | boolean | Promise<boolean>;
10
+ onCounterDown?: () => void;
11
+ isBackAllowed: boolean;
11
12
  };
12
13
 
13
14
  export const PaywallContext = createContext<PaywallContextValue | undefined>(
@@ -18,8 +19,9 @@ export function usePaywallContext(): PaywallContextValue {
18
19
  return (
19
20
  useContext(PaywallContext) ?? {
20
21
  products: [],
21
- selectedProductId: '',
22
22
  setSelectedProductId: () => {},
23
+ onCounterDown: () => {},
24
+ isBackAllowed: true,
23
25
  }
24
26
  );
25
27
  }
@@ -11,6 +11,10 @@ import { LocalizationParamsProvider } from '../../components/LocalizationParamsP
11
11
  import { useBuilderParams } from '../../components/BuilderProvider';
12
12
  import { PaywallContext } from './PaywallContext';
13
13
  import { useMockOSContext } from '../../mockOS/context/MockOSContextBase';
14
+ import type { Product } from '../../paywall/types/paywall-types';
15
+ import { useChangeDelayByPaywall } from '../../paywall/hooks/useChangeDelayByPaywall';
16
+ import { useHandleGoBack } from '../../paywall/hooks/useHandleGoBack';
17
+ import { useMockOSBackHandler } from '../../paywall/hooks/useMockOSBackHandler';
14
18
 
15
19
  function PaywallProvider({ node }: PaywallProviderComponentProps) {
16
20
  useLogRender('PaywallProvider');
@@ -57,6 +61,7 @@ function PaywallProvider({ node }: PaywallProviderComponentProps) {
57
61
  );
58
62
 
59
63
  const [selectedProductId, setSelectedProductId] = useState<string>('');
64
+ const [isBackAllowed, setIsBackAllowed] = useState<boolean>(false);
60
65
  useEffect(() => {
61
66
  const list = Array.isArray(products) ? products : [];
62
67
  if (list.length === 0) {
@@ -70,6 +75,9 @@ function PaywallProvider({ node }: PaywallProviderComponentProps) {
70
75
  }
71
76
  }, [products, selectedProductId]);
72
77
 
78
+ useChangeDelayByPaywall(node, setIsBackAllowed);
79
+ useMockOSBackHandler(isBackAllowed);
80
+
73
81
  const selectedProduct = useMemo(() => {
74
82
  const list = Array.isArray(products) ? products : [];
75
83
  return (
@@ -79,6 +87,9 @@ function PaywallProvider({ node }: PaywallProviderComponentProps) {
79
87
  }, [products, selectedProductId]);
80
88
 
81
89
  const handleClose = useCallback(() => {
90
+ if (!isBackAllowed) {
91
+ return;
92
+ }
82
93
  // Default: in MockOS go back (simulate dismissing paywall screen).
83
94
  // TODO: at react native merge it will change
84
95
  if (mockOS?.isEnabled) {
@@ -87,13 +98,22 @@ function PaywallProvider({ node }: PaywallProviderComponentProps) {
87
98
  mockOS.navigation('launchscreen');
88
99
  }
89
100
  }
90
- }, [mockOS]);
101
+ }, [mockOS, isBackAllowed]);
102
+
103
+ const handleGoBack = useCallback(() => {
104
+ if (!isBackAllowed) {
105
+ return false;
106
+ }
107
+ return true;
108
+ }, [isBackAllowed]);
109
+
110
+ useHandleGoBack(handleGoBack);
91
111
 
92
112
  const handleSubscribe = useCallback(
93
- async (product?: { productId?: string }) => {
113
+ async (product?: Product): Promise<boolean> => {
94
114
  // Host app override wins.
95
115
  if (onPaywallSubscribe) {
96
- const result = onPaywallSubscribe(product as any);
116
+ const result = onPaywallSubscribe(product);
97
117
  const ok =
98
118
  result instanceof Promise
99
119
  ? await result
@@ -105,7 +125,7 @@ function PaywallProvider({ node }: PaywallProviderComponentProps) {
105
125
  if (ok && mockOS?.isEnabled) {
106
126
  mockOS.goBack?.();
107
127
  }
108
- return;
128
+ return ok;
109
129
  }
110
130
 
111
131
  // Default: in MockOS show a native-like subscription prompt.
@@ -122,26 +142,44 @@ function PaywallProvider({ node }: PaywallProviderComponentProps) {
122
142
  // TODO: at react native merge it will change
123
143
  mockOS.goBack?.();
124
144
  }
145
+ return ok;
125
146
  }
147
+ return false;
126
148
  },
127
149
  [mockOS, onPaywallSubscribe],
128
150
  );
129
151
 
152
+ const handleCounterDown = useCallback(() => {
153
+ if (!isBackAllowed) {
154
+ return;
155
+ }
156
+ // Default: in MockOS go back (simulate dismissing paywall screen).
157
+ // TODO: at react native merge it will change
158
+ if (mockOS?.isEnabled) {
159
+ const canGoBack = mockOS.goBack();
160
+ if (!canGoBack) {
161
+ mockOS.navigation('launchscreen');
162
+ }
163
+ }
164
+ }, [isBackAllowed, mockOS]);
165
+
130
166
  const paywallContextValue = useMemo(
131
167
  () => ({
132
168
  products: Array.isArray(products) ? products : [],
133
- selectedProductId,
134
169
  setSelectedProductId,
135
170
  selectedProduct,
136
171
  onClose: handleClose,
137
- onSubscribe: handleSubscribe as any,
172
+ onSubscribe: handleSubscribe,
173
+ onCounterDown: handleCounterDown,
174
+ isBackAllowed,
138
175
  }),
139
176
  [
140
177
  products,
141
- selectedProductId,
142
178
  selectedProduct,
143
179
  handleClose,
144
180
  handleSubscribe,
181
+ handleCounterDown,
182
+ isBackAllowed,
145
183
  ],
146
184
  );
147
185
 
@@ -61,6 +61,7 @@ export interface PaywallProviderPropsGenerated {
61
61
  title?: string;
62
62
  description?: string;
63
63
  scrollable?: boolean;
64
+ delay?: number;
64
65
  };
65
66
  }
66
67
 
@@ -6,7 +6,8 @@
6
6
  "extends": "View",
7
7
  "attributes": {
8
8
  "title": "title",
9
- "description": "description"
9
+ "description": "description",
10
+ "delay": "number"
10
11
  }
11
12
  },
12
13
  "defaults": {
@@ -21,6 +22,15 @@
21
22
  ],
22
23
  "label": "Paywall Provider",
23
24
  "description": "Provider/wrapper for paywall screen components.",
25
+ "attributes": {
26
+ "delay": {
27
+ "label": "Back Delay (ms)",
28
+ "description": "Milliseconds before the paywall can be dismissed via back/close.",
29
+ "category": "other",
30
+ "specialCategory": null,
31
+ "sort": 1
32
+ }
33
+ },
24
34
  "mockableFeatures": {
25
35
  "products": true,
26
36
  "benefits": true
@@ -9823,11 +9823,13 @@ export const patterns = [
9823
9823
  style: {
9824
9824
  fontSize: '16',
9825
9825
  flexDirection: 'row',
9826
- position: 'relative',
9826
+ position: 'absolute',
9827
9827
  zIndex: 1,
9828
9828
  alignSelf: 'flex-start',
9829
9829
  flexGrow: 0,
9830
9830
  flexShrink: 0,
9831
+ top: '35@vs',
9832
+ left: '24@s',
9831
9833
  justifyContent: 'center',
9832
9834
  alignItems: 'center',
9833
9835
  },
@@ -10705,6 +10707,7 @@ export const patterns = [
10705
10707
  right: 'size',
10706
10708
  zIndex: 'number',
10707
10709
  },
10710
+ delay: 'number',
10708
10711
  },
10709
10712
  },
10710
10713
  meta: {
@@ -11004,6 +11007,14 @@ export const patterns = [
11004
11007
  specialCategory: null,
11005
11008
  sort: 26,
11006
11009
  },
11010
+ delay: {
11011
+ label: 'Back Delay (ms)',
11012
+ description:
11013
+ 'Milliseconds before the paywall can be dismissed via back/close.',
11014
+ category: 'other',
11015
+ specialCategory: null,
11016
+ sort: 1,
11017
+ },
11007
11018
  },
11008
11019
  mockableFeatures: { products: true, benefits: true },
11009
11020
  },
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { Device } from '../types/Device';
3
+ import { emitMockOSBackPress } from '../mockOS/backHandler';
3
4
  import { useMockOSContext } from '../mockOS/context/MockOSContextBase';
4
5
 
5
6
  type DeviceNavigationBarProps = {
@@ -37,6 +38,10 @@ export function DeviceNavigationBar({
37
38
  : 'rgba(0, 0, 0, 0.4)';
38
39
 
39
40
  function handleBackButton() {
41
+ if (!emitMockOSBackPress()) {
42
+ return;
43
+ }
44
+
40
45
  if (context) {
41
46
  const canGoBack = context.goBack();
42
47
  // If can't go back, go to launchscreen
package/src/index.ts CHANGED
@@ -27,11 +27,9 @@ export type {
27
27
 
28
28
  // Paywall hooks (RN-safe placeholders)
29
29
  export {
30
- usePaywallCounter,
31
- useCloseStatusPaywall,
32
- useCarouselOptionsSeperator,
33
30
  useCalculateLocalizedPrice,
34
31
  useDiscountRate,
32
+ useChangeDelayByPaywall,
35
33
  } from './paywall/hooks';
36
34
 
37
35
  // Context (RN-safe). In React Native, `products` should come from an IAP wrapper
@@ -0,0 +1,35 @@
1
+ export type MockOSBackHandler = () => boolean;
2
+
3
+ const backHandlers: MockOSBackHandler[] = [];
4
+
5
+ export function registerMockOSBackHandler(
6
+ handler: MockOSBackHandler,
7
+ ): () => void {
8
+ backHandlers.push(handler);
9
+
10
+ return () => {
11
+ const index = backHandlers.lastIndexOf(handler);
12
+ if (index >= 0) {
13
+ backHandlers.splice(index, 1);
14
+ }
15
+ };
16
+ }
17
+
18
+ export function emitMockOSBackPress(): boolean {
19
+ if (backHandlers.length === 0) {
20
+ return true;
21
+ }
22
+
23
+ for (let i = backHandlers.length - 1; i >= 0; i -= 1) {
24
+ const allow = backHandlers[i]?.();
25
+ if (!allow) {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ return true;
31
+ }
32
+
33
+ export function clearMockOSBackHandlers(): void {
34
+ backHandlers.length = 0;
35
+ }
@@ -71,7 +71,16 @@ export function ProjectMigrationPage({
71
71
  type="button"
72
72
  className="editor-button"
73
73
  disabled={!!migrating}
74
- onClick={() => onMigrateNow?.()}
74
+ onClick={() => {
75
+ console.info('[ProjectMigrationPage] onMigrateNow clicked', {
76
+ name,
77
+ projectVersion,
78
+ requiredVersion,
79
+ pendingMigrationsCount: pendingMigrations.length,
80
+ migrating: !!migrating,
81
+ });
82
+ onMigrateNow?.();
83
+ }}
75
84
  >
76
85
  {migrating ? 'Migrating…' : 'Migrate now'}
77
86
  </button>
@@ -289,6 +289,13 @@ export function ProjectPage({
289
289
  <EditorHeader
290
290
  onSaveProject={() => {
291
291
  try {
292
+ console.info('[ProjectPage] onSaveProject clicked', {
293
+ name: project.name,
294
+ hasOverrideProject: !!overrideProject,
295
+ hasEditorData: !!editorData,
296
+ hasResolvedProjectColors: !!resolvedProjectColors,
297
+ bypassValidation,
298
+ });
292
299
  logger.info('ProjectPage', 'save project', { name: project.name });
293
300
  if (onSaveProjectColors && resolvedProjectColors) {
294
301
  onSaveProjectColors(resolvedProjectColors);
@@ -1,5 +1,5 @@
1
- export { usePaywallCounter } from './usePaywallCounter';
2
- export { useCloseStatusPaywall } from './useCloseStatusPaywall';
3
- export { useCarouselOptionsSeperator } from './useCarouselOptionsSeperator';
4
1
  export { useCalculateLocalizedPrice } from './useCalculateLocalizedPrice';
5
2
  export { useDiscountRate } from './useDiscountRate';
3
+ export { useChangeDelayByPaywall } from './useChangeDelayByPaywall';
4
+ export { useHandleGoBack } from './useHandleGoBack';
5
+ export { useMockOSBackHandler } from './useMockOSBackHandler';
@@ -0,0 +1,25 @@
1
+ import { useEffect, useMemo } from 'react';
2
+ import type { NodeData } from '../../types/Node';
3
+
4
+ export function useChangeDelayByPaywall(
5
+ node: NodeData<{ delay?: number }> | null | undefined,
6
+ setIsBackAllowed: (value: boolean) => void,
7
+ ) {
8
+ const delay = useMemo(() => {
9
+ const delayAttribute = node?.attributes?.delay
10
+ ? parseInt(String(node?.attributes?.delay), 10)
11
+ : 1000;
12
+ return delayAttribute;
13
+ }, [node?.attributes?.delay]);
14
+
15
+ useEffect(() => {
16
+ if (!delay || delay <= 0) {
17
+ setIsBackAllowed(true);
18
+ return;
19
+ }
20
+ const timeout = setTimeout(() => {
21
+ setIsBackAllowed(true);
22
+ }, delay);
23
+ return () => clearTimeout(timeout);
24
+ }, [delay, setIsBackAllowed]);
25
+ }
@@ -0,0 +1,60 @@
1
+ import { useEffect } from 'react';
2
+ import { useMockOSContext } from '../../mockOS/context/MockOSContextBase';
3
+
4
+ export function useHandleGoBack(
5
+ handleGoBack: () => boolean | null | undefined,
6
+ ) {
7
+ const mockOS = useMockOSContext();
8
+
9
+ useEffect(() => {
10
+ if (!mockOS?.isEnabled) {
11
+ return;
12
+ }
13
+
14
+ const handleBackPress = (event?: KeyboardEvent | PopStateEvent) => {
15
+ const result = handleGoBack();
16
+
17
+ // Only allow navigation if result is explicitly true
18
+ if (result !== true) {
19
+ // Prevent navigation
20
+ if (event instanceof KeyboardEvent) {
21
+ event.preventDefault();
22
+ event.stopPropagation();
23
+ } else if (event instanceof PopStateEvent) {
24
+ // Prevent browser navigation by pushing state back
25
+ window.history.pushState(null, '', window.location.href);
26
+ }
27
+ return;
28
+ }
29
+
30
+ // Allow navigation - execute MockOS goBack
31
+ const canGoBack = mockOS.goBack();
32
+ if (!canGoBack) {
33
+ mockOS.navigation('launchscreen');
34
+ }
35
+ };
36
+
37
+ // Handle Escape key
38
+ const handleKeyDown = (e: KeyboardEvent) => {
39
+ if (e.key === 'Escape') {
40
+ handleBackPress(e);
41
+ }
42
+ };
43
+
44
+ // Handle browser back button
45
+ const handlePopState = (e: PopStateEvent) => {
46
+ handleBackPress(e);
47
+ };
48
+
49
+ // Push a state to track back navigation
50
+ window.history.pushState(null, '', window.location.href);
51
+
52
+ window.addEventListener('keydown', handleKeyDown);
53
+ window.addEventListener('popstate', handlePopState);
54
+
55
+ return () => {
56
+ window.removeEventListener('keydown', handleKeyDown);
57
+ window.removeEventListener('popstate', handlePopState);
58
+ };
59
+ }, [handleGoBack, mockOS]);
60
+ }
@@ -0,0 +1,9 @@
1
+ import { useEffect } from 'react';
2
+ import { registerMockOSBackHandler } from '../../mockOS/backHandler';
3
+
4
+ export function useMockOSBackHandler(isBackAllowed: boolean) {
5
+ useEffect(() => {
6
+ const unsubscribe = registerMockOSBackHandler(() => isBackAllowed);
7
+ return unsubscribe;
8
+ }, [isBackAllowed]);
9
+ }
@@ -1,6 +0,0 @@
1
- /**
2
- * Placeholder hook — will be implemented later.
3
- *
4
- * NOTE: name kept as `Seperator` to match existing external API usage.
5
- */
6
- export declare function useCarouselOptionsSeperator(): string;
@@ -1,4 +0,0 @@
1
- /**
2
- * Placeholder hook — will be implemented later.
3
- */
4
- export declare function useCloseStatusPaywall(): boolean;
@@ -1,4 +0,0 @@
1
- /**
2
- * Placeholder hook — will be implemented later.
3
- */
4
- export declare function usePaywallCounter(): number;
@@ -1,8 +0,0 @@
1
- /**
2
- * Placeholder hook — will be implemented later.
3
- *
4
- * NOTE: name kept as `Seperator` to match existing external API usage.
5
- */
6
- export function useCarouselOptionsSeperator(): string {
7
- return '';
8
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * Placeholder hook — will be implemented later.
3
- */
4
- export function useCloseStatusPaywall(): boolean {
5
- return false;
6
- }