@developer_tribe/react-builder 1.2.39 → 1.2.41

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 (93) hide show
  1. package/dist/attributes-editor/FallbackLocalizationField.d.ts +6 -0
  2. package/dist/build-components/NavigationBarColor/NavigationBarColorProps.generated.d.ts +1 -40
  3. package/dist/build-components/StatusBarColor/StatusBarColorProps.generated.d.ts +1 -1
  4. package/dist/build-components/patterns.generated.d.ts +21 -344
  5. package/dist/components/BuilderProvider.d.ts +1 -0
  6. package/dist/components/DeviceButton.d.ts +4 -1
  7. package/dist/index.cjs.js +1 -1
  8. package/dist/index.cjs.js.map +1 -1
  9. package/dist/index.esm.js +1 -1
  10. package/dist/index.esm.js.map +1 -1
  11. package/dist/index.web.cjs.js +4 -4
  12. package/dist/index.web.cjs.js.map +1 -1
  13. package/dist/index.web.d.ts +8 -0
  14. package/dist/index.web.esm.js +4 -4
  15. package/dist/index.web.esm.js.map +1 -1
  16. package/dist/mockOS/context/MockOSContext.d.ts +3 -1
  17. package/dist/product-base/types.d.ts +3 -0
  18. package/dist/size-matters/index.d.ts +1 -1
  19. package/dist/store.d.ts +31 -0
  20. package/dist/styles.css +1 -1
  21. package/dist/types/Device.d.ts +5 -0
  22. package/dist/types/PreviewConfig.d.ts +1 -1
  23. package/dist/utils/extractTextStyle/extractTextStyle.d.ts +1 -0
  24. package/dist/utils/extractViewStyle/extractViewStyle.d.ts +1 -0
  25. package/package.json +1 -1
  26. package/scripts/prebuild/assets/prompt_scheme.md +7 -0
  27. package/scripts/public/bin.js +0 -0
  28. package/src/DeviceMockFrame.tsx +8 -0
  29. package/src/RenderPage.tsx +3 -0
  30. package/src/assets/devices.json +747 -183
  31. package/src/assets/meta.json +1 -1
  32. package/src/assets/prompt-scheme-onboard.generated.ts +1 -1
  33. package/src/assets/prompt-scheme-paywall.generated.ts +1 -1
  34. package/src/assets/samples/carousel-sample.json +30 -26
  35. package/src/assets/samples/paywall-1.json +31 -31
  36. package/src/assets/samples/paywall-2.json +28 -28
  37. package/src/assets/samples/paywall-app-delete-offer.json +29 -29
  38. package/src/assets/samples/paywall-app-open-offer.json +29 -29
  39. package/src/assets/samples/paywall-back-offer.json +28 -28
  40. package/src/assets/samples/paywall-notification-offer.json +28 -28
  41. package/src/assets/samples/simple-1.json +4 -4
  42. package/src/assets/samples/simple-2.json +25 -25
  43. package/src/assets/samples/unmigrated-builder-1.1.1.json +7 -7
  44. package/src/assets/samples/unmigrated-builder1.json +4 -4
  45. package/src/assets/samples/unvalidated-builder1.json +4 -4
  46. package/src/assets/samples/unvalidated-crash1.json +2 -2
  47. package/src/assets/samples/unvalidated-crashcomponent1.json +2 -2
  48. package/src/assets/samples/vpn-onboard-1.json +30 -30
  49. package/src/assets/samples/vpn-onboard-2.json +30 -30
  50. package/src/assets/samples/vpn-onboard-3.json +27 -27
  51. package/src/assets/samples/vpn-onboard-4.json +27 -27
  52. package/src/assets/samples/vpn-onboard-5.json +40 -40
  53. package/src/assets/samples/vpn-onboard-6.json +30 -30
  54. package/src/assets/samples/vpn-onboard-7.json +29 -29
  55. package/src/attribute-analyser/style/web/useExtractImageStyle.ts +8 -3
  56. package/src/attribute-analyser/style/web/useExtractViewStyle.ts +8 -3
  57. package/src/attributes-editor/AttributesEditorView.tsx +17 -6
  58. package/src/attributes-editor/FallbackLocalizationField.tsx +384 -0
  59. package/src/build-components/CarouselDots/CarouselDots.tsx +8 -3
  60. package/src/build-components/Main/Main.tsx +3 -1
  61. package/src/build-components/NavigationBarColor/NavigationBarColor.tsx +15 -1
  62. package/src/build-components/NavigationBarColor/NavigationBarColorProps.generated.ts +1 -52
  63. package/src/build-components/NavigationBarColor/pattern.json +11 -2
  64. package/src/build-components/OnboardDot/OnboardDot.tsx +3 -2
  65. package/src/build-components/PaywallCloseButton/pattern.json +1 -0
  66. package/src/build-components/StatusBarColor/StatusBarColor.tsx +15 -1
  67. package/src/build-components/StatusBarColor/StatusBarColorProps.generated.ts +1 -1
  68. package/src/build-components/StatusBarColor/pattern.json +10 -1
  69. package/src/build-components/patterns.generated.ts +25 -364
  70. package/src/components/BottomBar.tsx +135 -31
  71. package/src/components/BuilderProvider.tsx +1 -0
  72. package/src/components/DeviceButton.tsx +35 -0
  73. package/src/components/EditorHeader.tsx +16 -1
  74. package/src/hooks/useLocalize.ts +3 -1
  75. package/src/hooks/useSafeAreaViewStyle.ts +24 -4
  76. package/src/index.web.ts +19 -0
  77. package/src/mockOS/context/MockOSContext.tsx +41 -13
  78. package/src/modals/DeviceSelectorModal.tsx +94 -10
  79. package/src/modals/InspectModal.tsx +112 -4
  80. package/src/product-base/buildPaywallLocalizationParams.ts +3 -0
  81. package/src/product-base/extractAndroidParams.ts +38 -8
  82. package/src/product-base/types.ts +3 -0
  83. package/src/size-matters/index.ts +15 -9
  84. package/src/store.ts +66 -0
  85. package/src/styles/modals/_product-edit-modal.scss +2 -2
  86. package/src/types/Device.ts +5 -0
  87. package/src/types/PreviewConfig.ts +6 -0
  88. package/src/utils/analyseNodeByPatterns.ts +6 -2
  89. package/src/utils/extractTextStyle/extractTextStyle.ts +3 -1
  90. package/src/utils/extractTextStyle/extractTextStyleNative.ts +1 -1
  91. package/src/utils/extractViewStyle/extractViewStyle.ts +19 -5
  92. package/src/utils/extractViewStyle/extractViewStyleNative.ts +5 -1
  93. package/src/utils/replaceLocalizationParams.ts +5 -7
@@ -11,12 +11,16 @@ type DeviceButtonProps = {
11
11
  device: Device;
12
12
  selectedDevice: Device | null;
13
13
  onSelect: (device: Device) => void;
14
+ isFavorite?: boolean;
15
+ onToggleFavorite?: (device: Device, e: React.MouseEvent) => void;
14
16
  };
15
17
 
16
18
  export function DeviceButton({
17
19
  device,
18
20
  selectedDevice,
19
21
  onSelect,
22
+ isFavorite,
23
+ onToggleFavorite,
20
24
  }: DeviceButtonProps) {
21
25
  const platformIcon = platformIcons[device.platform];
22
26
  const aspect =
@@ -36,6 +40,37 @@ export function DeviceButton({
36
40
  }`}
37
41
  onClick={() => onSelect(device)}
38
42
  >
43
+ {onToggleFavorite && (
44
+ <span
45
+ className="editor-device-button__favorite"
46
+ onClick={(e) => {
47
+ e.stopPropagation();
48
+ onToggleFavorite(device, e);
49
+ }}
50
+ title={isFavorite ? 'Remove from favorites' : 'Add to favorites'}
51
+ style={{
52
+ position: 'absolute',
53
+ top: 4,
54
+ right: 4,
55
+ cursor: 'pointer',
56
+ opacity: isFavorite ? 1 : 0.3,
57
+ color: isFavorite ? '#ecc538' : 'currentColor',
58
+ }}
59
+ >
60
+ <svg
61
+ width="14"
62
+ height="14"
63
+ viewBox="0 0 24 24"
64
+ fill={isFavorite ? 'currentColor' : 'none'}
65
+ stroke="currentColor"
66
+ strokeWidth="2"
67
+ strokeLinecap="round"
68
+ strokeLinejoin="round"
69
+ >
70
+ <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
71
+ </svg>
72
+ </span>
73
+ )}
39
74
  {device.name} <br />
40
75
  {device.width}×{device.height} ({aspect})
41
76
  {platformIcon && (
@@ -40,6 +40,8 @@ export function EditorHeader({
40
40
  setLocalization,
41
41
  setBaseSize,
42
42
  setProjectColors,
43
+ favoriteDevices,
44
+ toggleFavoriteDevice,
43
45
  } = useRenderStore((s) => ({
44
46
  device: s.device,
45
47
  setDevice: s.setDevice,
@@ -47,8 +49,19 @@ export function EditorHeader({
47
49
  setLocalization: s.setLocalization,
48
50
  setBaseSize: s.setBaseSize,
49
51
  setProjectColors: s.setProjectColors,
52
+ favoriteDevices: s.favoriteDevices || [],
53
+ toggleFavoriteDevice: s.toggleFavoriteDevice,
50
54
  }));
51
55
 
56
+ const headerDevices = useMemo(() => {
57
+ const favs = favoriteDevices
58
+ .map((name) => devices.find((d) => d.name === name))
59
+ .filter((d): d is Device => d !== undefined);
60
+
61
+ const others = devices.filter((d) => !favoriteDevices.includes(d.name));
62
+ return [...favs, ...others].slice(0, 5);
63
+ }, [favoriteDevices]);
64
+
52
65
  const sortedSamples = useMemo(() => {
53
66
  const weight = (t?: Project['type']) => {
54
67
  if (t === 'paywall') return 0;
@@ -249,12 +262,14 @@ export function EditorHeader({
249
262
  aria-label="Editor utility header"
250
263
  >
251
264
  <div className="editor-header__devices">
252
- {devices.slice(0, 5).map((deviceOption: Device) => (
265
+ {headerDevices.map((deviceOption: Device) => (
253
266
  <DeviceButton
254
267
  key={deviceOption.name}
255
268
  selectedDevice={selectedDevice}
256
269
  onSelect={setDevice}
257
270
  device={deviceOption}
271
+ isFavorite={favoriteDevices.includes(deviceOption.name)}
272
+ onToggleFavorite={(d) => toggleFavoriteDevice(d.name)}
258
273
  />
259
274
  ))}
260
275
  <button
@@ -23,7 +23,9 @@ export function useLocalize(options?: {
23
23
 
24
24
  return useCallback(
25
25
  (keyOrText: string) => {
26
- const raw = localization?.[defaultLanguage]?.[keyOrText] ?? keyOrText;
26
+ const langMap = localization?.[defaultLanguage];
27
+ const raw = langMap?.[keyOrText] ?? keyOrText;
28
+
27
29
  return replaceLocalizationParams(raw, params);
28
30
  },
29
31
  [defaultLanguage, localization, params],
@@ -1,5 +1,6 @@
1
1
  import { useMemo } from 'react';
2
2
  import type { Device } from '../types/Device';
3
+ import { useRenderStore } from '../store';
3
4
 
4
5
  function addInset(
5
6
  base: React.CSSProperties['paddingTop'],
@@ -30,17 +31,30 @@ export function useSafeAreaViewStyle(
30
31
  enabled: boolean,
31
32
  device?: Device,
32
33
  ) {
34
+ const statusBarOverrideTranslucent = useRenderStore(
35
+ (s) => s.statusBarOverrideTranslucent,
36
+ );
37
+ const navBarOverrideTranslucent = useRenderStore(
38
+ (s) => s.navBarOverrideTranslucent,
39
+ );
40
+
33
41
  return useMemo(() => {
34
42
  if (!enabled) return baseStyle;
35
43
 
36
44
  const [insetTop, insetRight, , insetLeft] = device?.insets ?? [0, 0, 0, 0];
37
45
 
38
46
  // Match DeviceMockFrame fallbacks: status bar overlays content, so we treat it as top safe area.
39
- const top =
40
- insetTop || (device?.platform === 'ios' ? 20 : device?.platform ? 24 : 0);
47
+ // If translucent is true, we do not apply safe area to top.
48
+ const top = statusBarOverrideTranslucent
49
+ ? 0
50
+ : insetTop ||
51
+ (device?.platform === 'ios' ? 20 : device?.platform ? 24 : 0);
41
52
 
42
53
  // Bottom safe area is handled visually by the mock navigation bar area, which takes layout space.
43
- // So we intentionally don't add bottom padding here to avoid double-spacing.
54
+ // However, if we need to support insets for bottom when not rendered by the mock navigation bar,
55
+ // or if the navigation bar is translucent, we apply the inset here. But the mock frame handles it for now.
56
+ // Since Main acts as the safe area provider, if navigation bar is translucent it shouldn't have bottom inset,
57
+ // but right now it is 0. If it were relying on bottom inset, we would check `navBarOverrideTranslucent ? 0 : bottom`.
44
58
  const right = insetRight ?? 0;
45
59
  const left = insetLeft ?? 0;
46
60
  const bottom = 0;
@@ -60,5 +74,11 @@ export function useSafeAreaViewStyle(
60
74
  width: subtractInset(baseStyle.width ?? '100%', horizontal),
61
75
  height: subtractInset(baseStyle.height ?? '100%', vertical),
62
76
  };
63
- }, [enabled, baseStyle, device]);
77
+ }, [
78
+ enabled,
79
+ baseStyle,
80
+ device,
81
+ statusBarOverrideTranslucent,
82
+ navBarOverrideTranslucent,
83
+ ]);
64
84
  }
package/src/index.web.ts CHANGED
@@ -11,3 +11,22 @@ export * from './index';
11
11
  export * from './build-components';
12
12
  export { default as useNode } from './build-components/useNode';
13
13
  export type { EventObjectGenerated } from './build-components/OnboardButton/OnboardButtonProps.generated';
14
+
15
+ // Host-app API: let parent apps inject a custom "Text" field renderer.
16
+ import { useRenderStore } from './store';
17
+ import type { ComponentType } from 'react';
18
+ import type { LocalizationApiConfig, LanguageColumn } from './store';
19
+
20
+ export function setBuilderStringChildrenField(
21
+ comp?: ComponentType<{ value: string; onChange: (v: string) => void }>,
22
+ ) {
23
+ useRenderStore.getState().setRenderStringChildrenField(comp);
24
+ }
25
+
26
+ export function setBuilderLocalizationConfig(config?: LocalizationApiConfig) {
27
+ useRenderStore.getState().setLocalizationApiConfig(config);
28
+ }
29
+
30
+ export function setBuilderLanguageColumns(cols: LanguageColumn[]) {
31
+ useRenderStore.getState().setLanguageColumns(cols);
32
+ }
@@ -32,12 +32,14 @@ interface MockOSProviderProps {
32
32
  statusBarBackgroundColor: string;
33
33
  statusBarPlatform: Device['platform'];
34
34
  statusBarIsDark: boolean;
35
+ statusBarTranslucent?: boolean;
35
36
  // Navigation Bar props
36
37
  navBarHeight: number;
37
38
  navBarBackgroundColor: string;
38
39
  navBarPlatform: Device['platform'];
39
40
  navBarNavigationBarType: Device['navigationBarType'];
40
41
  navBarIsDark: boolean;
42
+ navBarTranslucent?: boolean;
41
43
  // Insets
42
44
  insetLeft: number;
43
45
  insetRight: number;
@@ -51,11 +53,13 @@ export function MockOSProvider({
51
53
  statusBarBackgroundColor,
52
54
  statusBarPlatform,
53
55
  statusBarIsDark,
56
+ statusBarTranslucent = false,
54
57
  navBarHeight,
55
58
  navBarBackgroundColor,
56
59
  navBarPlatform,
57
60
  navBarNavigationBarType,
58
61
  navBarIsDark,
62
+ navBarTranslucent = false,
59
63
  }: MockOSProviderProps) {
60
64
  const [permission, setPermission] = useState<PermissionType | string | null>(
61
65
  null,
@@ -217,31 +221,55 @@ export function MockOSProvider({
217
221
  onCancel={() => resolveSubscriptionPurchase(false)}
218
222
  />
219
223
  )}
220
- <DeviceStatusBar
221
- height={statusBarHeight}
222
- backgroundColor={statusBarBackgroundColor}
223
- platform={statusBarPlatform}
224
- isDark={statusBarIsDark}
225
- />
224
+ <div
225
+ className="device-status-bar-container"
226
+ style={{
227
+ position: statusBarTranslucent ? 'absolute' : 'relative',
228
+ top: 0,
229
+ left: 0,
230
+ right: 0,
231
+ zIndex: 10,
232
+ }}
233
+ >
234
+ <DeviceStatusBar
235
+ height={statusBarHeight}
236
+ backgroundColor={statusBarBackgroundColor}
237
+ platform={statusBarPlatform}
238
+ isDark={statusBarIsDark}
239
+ />
240
+ </div>
226
241
  <div
227
242
  className="device-content"
228
243
  style={{
229
244
  flex: 1,
230
245
  overflow: 'hidden',
231
246
  position: 'relative',
247
+ // If nav bar is translucent, let the content area stretch under it
248
+ marginBottom: navBarTranslucent ? -navBarHeight : 0,
232
249
  }}
233
250
  >
234
251
  <MockOSRouter childrenBelongTo="app" appName={appName}>
235
252
  {children}
236
253
  </MockOSRouter>
237
254
  </div>
238
- <DeviceNavigationBar
239
- height={navBarHeight}
240
- backgroundColor={navBarBackgroundColor}
241
- platform={navBarPlatform}
242
- navigationBarType={navBarNavigationBarType}
243
- isDark={navBarIsDark}
244
- />
255
+ <div
256
+ className="device-navigation-bar-container"
257
+ style={{
258
+ position: navBarTranslucent ? 'absolute' : 'relative',
259
+ bottom: 0,
260
+ left: 0,
261
+ right: 0,
262
+ zIndex: 10,
263
+ }}
264
+ >
265
+ <DeviceNavigationBar
266
+ height={navBarHeight}
267
+ backgroundColor={navBarBackgroundColor}
268
+ platform={navBarPlatform}
269
+ navigationBarType={navBarNavigationBarType}
270
+ isDark={navBarIsDark}
271
+ />
272
+ </div>
245
273
  </MockOSContext.Provider>
246
274
  );
247
275
  }
@@ -1,7 +1,8 @@
1
- import React from 'react';
1
+ import React, { useMemo, useState } from 'react';
2
2
  import { Device } from '../types/Device';
3
3
  import Modal from './Modal';
4
4
  import { DeviceButton } from '../components/DeviceButton';
5
+ import { useRenderStore } from '../store';
5
6
 
6
7
  type DeviceSelectorModalProps = {
7
8
  devices: Device[];
@@ -16,11 +17,36 @@ export function DeviceSelectorModal({
16
17
  onSelect,
17
18
  onClose,
18
19
  }: DeviceSelectorModalProps) {
20
+ const [searchTerm, setSearchTerm] = useState('');
21
+ const { favoriteDevices, toggleFavoriteDevice } = useRenderStore((state) => ({
22
+ favoriteDevices: state.favoriteDevices || [],
23
+ toggleFavoriteDevice: state.toggleFavoriteDevice,
24
+ }));
25
+
19
26
  const handleDeviceSelect = (device: Device) => {
20
27
  onSelect(device);
21
28
  onClose();
22
29
  };
23
30
 
31
+ const filteredDevices = useMemo(() => {
32
+ return devices.filter((device) =>
33
+ device.name.toLowerCase().includes(searchTerm.toLowerCase()),
34
+ );
35
+ }, [devices, searchTerm]);
36
+
37
+ const { favorites, others } = useMemo(() => {
38
+ const favs: Device[] = [];
39
+ const rest: Device[] = [];
40
+ filteredDevices.forEach((device) => {
41
+ if (favoriteDevices.includes(device.name)) {
42
+ favs.push(device);
43
+ } else {
44
+ rest.push(device);
45
+ }
46
+ });
47
+ return { favorites: favs, others: rest };
48
+ }, [filteredDevices, favoriteDevices]);
49
+
24
50
  return (
25
51
  <Modal
26
52
  onClose={onClose}
@@ -40,15 +66,73 @@ export function DeviceSelectorModal({
40
66
  Close
41
67
  </button>
42
68
  </div>
43
- <div className="device-selector-modal__grid" role="list">
44
- {devices.map((device) => (
45
- <DeviceButton
46
- key={device.name}
47
- device={device}
48
- selectedDevice={selectedDevice}
49
- onSelect={handleDeviceSelect}
50
- />
51
- ))}
69
+ <div
70
+ className="device-selector-modal__search"
71
+ style={{ padding: '0 16px', marginBottom: '8px' }}
72
+ >
73
+ <input
74
+ type="text"
75
+ placeholder="Search devices..."
76
+ value={searchTerm}
77
+ onChange={(e) => setSearchTerm(e.target.value)}
78
+ className="editor-input"
79
+ style={{ width: '100%', padding: '8px', boxSizing: 'border-box' }}
80
+ />
81
+ </div>
82
+ <div
83
+ className="device-selector-modal__body"
84
+ style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}
85
+ >
86
+ {favorites.length > 0 && (
87
+ <section>
88
+ <h4
89
+ style={{
90
+ margin: '0 16px 8px',
91
+ fontSize: '14px',
92
+ fontWeight: 'bold',
93
+ }}
94
+ >
95
+ Favorites
96
+ </h4>
97
+ <div className="device-selector-modal__grid" role="list">
98
+ {favorites.map((device) => (
99
+ <DeviceButton
100
+ key={`fav-${device.name}`}
101
+ device={device}
102
+ selectedDevice={selectedDevice}
103
+ onSelect={handleDeviceSelect}
104
+ isFavorite={true}
105
+ onToggleFavorite={(d) => toggleFavoriteDevice(d.name)}
106
+ />
107
+ ))}
108
+ </div>
109
+ </section>
110
+ )}
111
+ <section>
112
+ {favorites.length > 0 && (
113
+ <h4
114
+ style={{
115
+ margin: '0 16px 8px',
116
+ fontSize: '14px',
117
+ fontWeight: 'bold',
118
+ }}
119
+ >
120
+ All Devices
121
+ </h4>
122
+ )}
123
+ <div className="device-selector-modal__grid" role="list">
124
+ {others.map((device) => (
125
+ <DeviceButton
126
+ key={`other-${device.name}`}
127
+ device={device}
128
+ selectedDevice={selectedDevice}
129
+ onSelect={handleDeviceSelect}
130
+ isFavorite={false}
131
+ onToggleFavorite={(d) => toggleFavoriteDevice(d.name)}
132
+ />
133
+ ))}
134
+ </div>
135
+ </section>
52
136
  </div>
53
137
  </Modal>
54
138
  );
@@ -113,8 +113,41 @@ function LocalizationsTab({
113
113
  );
114
114
  }
115
115
 
116
+ const handleDownloadCSV = () => {
117
+ let csvContent = 'Key,Value,Source\n';
118
+ allKeys.forEach((key) => {
119
+ const hasCustom = key in langKeys;
120
+ const customValue = langKeys[key];
121
+ const defaultValue = defaultLangKeys[key];
122
+
123
+ const isCustom =
124
+ hasCustom && customValue !== defaultValue && customValue !== undefined;
125
+ const value = isCustom ? customValue : (defaultValue ?? '');
126
+
127
+ const escape = (str: string) => `"${String(str).replace(/"/g, '""')}"`;
128
+ csvContent += `${escape(key)},${escape(String(value))},${isCustom ? 'spreadsheet' : 'fallback'}\n`;
129
+ });
130
+
131
+ downloadCSV(`localizations-${language}.csv`, csvContent);
132
+ };
133
+
116
134
  return (
117
135
  <div className="inspect-modal__table-wrap">
136
+ <div
137
+ style={{
138
+ display: 'flex',
139
+ justifyContent: 'flex-end',
140
+ marginBottom: '8px',
141
+ }}
142
+ >
143
+ <button
144
+ type="button"
145
+ className="editor-button"
146
+ onClick={handleDownloadCSV}
147
+ >
148
+ Download CSV
149
+ </button>
150
+ </div>
118
151
  <table className="inspect-modal__table">
119
152
  <thead>
120
153
  <tr>
@@ -148,7 +181,7 @@ function LocalizationsTab({
148
181
  <span style={{ color: 'red' }}>!</span> fallback
149
182
  </>
150
183
  ) : (
151
- 'custom'
184
+ 'spreadsheet'
152
185
  )}
153
186
  </span>
154
187
  </td>
@@ -179,13 +212,43 @@ function ParamsTab({
179
212
  if (!hasAny) {
180
213
  return (
181
214
  <p className="inspect-modal__empty">
182
- No params available. Params are populated inside a PaywallProvider.
215
+ No params available. Params are populated inside a ParamsProvider.
183
216
  </p>
184
217
  );
185
218
  }
186
219
 
220
+ const handleDownloadCSV = () => {
221
+ let csvContent = 'Param,Value,Type\n';
222
+ const escape = (str: string) => `"${String(str).replace(/"/g, '""')}"`;
223
+
224
+ flatEntries.forEach(([key, value]) => {
225
+ csvContent += `${escape(key)},${escape(formatValue(value))},Flat\n`;
226
+ });
227
+
228
+ nestedEntries.forEach(([key, value]) => {
229
+ csvContent += `${escape(key)},${escape(formatValue(value))},Nested\n`;
230
+ });
231
+
232
+ downloadCSV('params.csv', csvContent);
233
+ };
234
+
187
235
  return (
188
236
  <div className="inspect-modal__table-wrap">
237
+ <div
238
+ style={{
239
+ display: 'flex',
240
+ justifyContent: 'flex-end',
241
+ marginBottom: '8px',
242
+ }}
243
+ >
244
+ <button
245
+ type="button"
246
+ className="editor-button"
247
+ onClick={handleDownloadCSV}
248
+ >
249
+ Download CSV
250
+ </button>
251
+ </div>
189
252
  <table className="inspect-modal__table">
190
253
  <thead>
191
254
  <tr>
@@ -249,8 +312,29 @@ function ColorsTab({
249
312
  return <p className="inspect-modal__empty">No project colors defined.</p>;
250
313
  }
251
314
 
315
+ const handleDownloadJSON = () => {
316
+ const data = projectColors ?? {};
317
+ const jsonContent = JSON.stringify(data, null, 2);
318
+ downloadJSON('colors.json', jsonContent);
319
+ };
320
+
252
321
  return (
253
322
  <div className="inspect-modal__table-wrap">
323
+ <div
324
+ style={{
325
+ display: 'flex',
326
+ justifyContent: 'flex-end',
327
+ marginBottom: '8px',
328
+ }}
329
+ >
330
+ <button
331
+ type="button"
332
+ className="editor-button"
333
+ onClick={handleDownloadJSON}
334
+ >
335
+ Download JSON
336
+ </button>
337
+ </div>
254
338
  {staticEntries.length > 0 && (
255
339
  <>
256
340
  <h4 className="inspect-modal__section-title">Static Colors</h4>
@@ -284,7 +368,7 @@ function ColorsTab({
284
368
  <span style={{ color: 'red' }}>!</span> fallback
285
369
  </>
286
370
  ) : (
287
- 'custom'
371
+ 'added to project'
288
372
  )}
289
373
  </span>
290
374
  </td>
@@ -331,7 +415,7 @@ function ColorsTab({
331
415
  <span style={{ color: 'red' }}>!</span> fallback
332
416
  </>
333
417
  ) : (
334
- 'custom'
418
+ 'added to project'
335
419
  )}
336
420
  </span>
337
421
  </td>
@@ -357,3 +441,27 @@ function formatValue(v: unknown): string {
357
441
  if (typeof v === 'number' || typeof v === 'boolean') return String(v);
358
442
  return JSON.stringify(v);
359
443
  }
444
+
445
+ function downloadCSV(filename: string, csvContent: string) {
446
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
447
+ const url = URL.createObjectURL(blob);
448
+ const link = document.createElement('a');
449
+ link.setAttribute('href', url);
450
+ link.setAttribute('download', filename);
451
+ document.body.appendChild(link);
452
+ link.click();
453
+ document.body.removeChild(link);
454
+ }
455
+
456
+ function downloadJSON(filename: string, jsonContent: string) {
457
+ const blob = new Blob([jsonContent], {
458
+ type: 'application/json;charset=utf-8;',
459
+ });
460
+ const url = URL.createObjectURL(blob);
461
+ const link = document.createElement('a');
462
+ link.setAttribute('href', url);
463
+ link.setAttribute('download', filename);
464
+ document.body.appendChild(link);
465
+ link.click();
466
+ document.body.removeChild(link);
467
+ }
@@ -105,5 +105,8 @@ export function buildPaywallLocalizationParams(
105
105
  productCurreny: extractedParams.currency,
106
106
  productId: String(product.productId ?? ''),
107
107
  productSelected: 'true',
108
+ subscribe: hasTrial
109
+ ? resolve('base.builder.paywall.button.subscribe-free')
110
+ : resolve('base.builder.paywall.button.subscribe'),
108
111
  };
109
112
  }
@@ -70,7 +70,7 @@ export function extractAndroidParams(
70
70
  iapLogger.warn(['extractAndroidParams'], 'No offers found in product', {
71
71
  productId: product.id || product.productId,
72
72
  });
73
- return getEmptyParams();
73
+ return getFallbackParams(product);
74
74
  }
75
75
 
76
76
  const pricingPhases =
@@ -81,7 +81,7 @@ export function extractAndroidParams(
81
81
  productId: product.id || product.productId,
82
82
  offerId: selectedOffer.id,
83
83
  });
84
- return getEmptyParams();
84
+ return getFallbackParams(product);
85
85
  }
86
86
 
87
87
  // Trial phase: priceAmountMicros === "0"
@@ -105,7 +105,7 @@ export function extractAndroidParams(
105
105
  productId: product.id || product.productId,
106
106
  pricingPhasesCount: pricingPhases.length,
107
107
  });
108
- return getEmptyParams();
108
+ return getFallbackParams(product);
109
109
  }
110
110
 
111
111
  const regularPeriod = parseBillingPeriod(regularPhase.billingPeriod);
@@ -142,15 +142,31 @@ export function extractAndroidParams(
142
142
  }
143
143
 
144
144
  const discountPercentage = calculateDiscount(price, promoPrice);
145
- const priceNum = parseFloat(price);
145
+
146
+ // Apply root properties if they exist (allows mock edits to immediately bypass deep structures)
147
+ const finalPrice =
148
+ typeof product.price === 'string' && product.price !== ''
149
+ ? product.price
150
+ : price;
151
+ const finalCurrency =
152
+ typeof product.currency === 'string' && product.currency !== ''
153
+ ? product.currency
154
+ : currency;
155
+ const finalLocalizedPrice =
156
+ typeof product.localizedPrice === 'string' &&
157
+ product.localizedPrice !== ''
158
+ ? product.localizedPrice
159
+ : localizedPrice;
160
+
161
+ const priceNum = parseFloat(finalPrice);
146
162
  const pricePerMonth = calculatePricePerMonth(priceNum, regularPeriod.unit);
147
163
  const pricePerYear = calculatePricePerYear(priceNum, regularPeriod.unit);
148
164
 
149
165
  return {
150
- price,
166
+ price: finalPrice,
151
167
  promoPrice,
152
- currency,
153
- localizedPrice,
168
+ currency: finalCurrency,
169
+ localizedPrice: finalLocalizedPrice,
154
170
  period: regularPeriod.unit,
155
171
  periodValue: String(regularPeriod.value),
156
172
  periodType,
@@ -173,10 +189,24 @@ export function extractAndroidParams(
173
189
  error: error instanceof Error ? error.message : String(error),
174
190
  },
175
191
  );
176
- return getEmptyParams();
192
+ return getFallbackParams(product);
177
193
  }
178
194
  }
179
195
 
196
+ /** Boş params ama mock product price'ları ile birleştirilmiş (fallback) */
197
+ function getFallbackParams(product: Product): AndroidParams {
198
+ const price = String(product.price || product.localizedPrice || '').replace(
199
+ /[^0-9.]/g,
200
+ '',
201
+ );
202
+ return {
203
+ ...getEmptyParams(),
204
+ price,
205
+ currency: product.currency || product.currencyCode || '',
206
+ localizedPrice: product.localizedPrice || '',
207
+ };
208
+ }
209
+
180
210
  /** Boş params döner (fallback) */
181
211
  function getEmptyParams(): AndroidParams {
182
212
  return {