@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.
- package/dist/attributes-editor/FallbackLocalizationField.d.ts +6 -0
- package/dist/build-components/NavigationBarColor/NavigationBarColorProps.generated.d.ts +1 -40
- package/dist/build-components/StatusBarColor/StatusBarColorProps.generated.d.ts +1 -1
- package/dist/build-components/patterns.generated.d.ts +21 -344
- package/dist/components/BuilderProvider.d.ts +1 -0
- package/dist/components/DeviceButton.d.ts +4 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.web.cjs.js +4 -4
- package/dist/index.web.cjs.js.map +1 -1
- package/dist/index.web.d.ts +8 -0
- package/dist/index.web.esm.js +4 -4
- package/dist/index.web.esm.js.map +1 -1
- package/dist/mockOS/context/MockOSContext.d.ts +3 -1
- package/dist/product-base/types.d.ts +3 -0
- package/dist/size-matters/index.d.ts +1 -1
- package/dist/store.d.ts +31 -0
- package/dist/styles.css +1 -1
- package/dist/types/Device.d.ts +5 -0
- package/dist/types/PreviewConfig.d.ts +1 -1
- package/dist/utils/extractTextStyle/extractTextStyle.d.ts +1 -0
- package/dist/utils/extractViewStyle/extractViewStyle.d.ts +1 -0
- package/package.json +1 -1
- package/scripts/prebuild/assets/prompt_scheme.md +7 -0
- package/scripts/public/bin.js +0 -0
- package/src/DeviceMockFrame.tsx +8 -0
- package/src/RenderPage.tsx +3 -0
- package/src/assets/devices.json +747 -183
- package/src/assets/meta.json +1 -1
- package/src/assets/prompt-scheme-onboard.generated.ts +1 -1
- package/src/assets/prompt-scheme-paywall.generated.ts +1 -1
- package/src/assets/samples/carousel-sample.json +30 -26
- package/src/assets/samples/paywall-1.json +31 -31
- package/src/assets/samples/paywall-2.json +28 -28
- package/src/assets/samples/paywall-app-delete-offer.json +29 -29
- package/src/assets/samples/paywall-app-open-offer.json +29 -29
- package/src/assets/samples/paywall-back-offer.json +28 -28
- package/src/assets/samples/paywall-notification-offer.json +28 -28
- package/src/assets/samples/simple-1.json +4 -4
- package/src/assets/samples/simple-2.json +25 -25
- package/src/assets/samples/unmigrated-builder-1.1.1.json +7 -7
- package/src/assets/samples/unmigrated-builder1.json +4 -4
- package/src/assets/samples/unvalidated-builder1.json +4 -4
- package/src/assets/samples/unvalidated-crash1.json +2 -2
- package/src/assets/samples/unvalidated-crashcomponent1.json +2 -2
- package/src/assets/samples/vpn-onboard-1.json +30 -30
- package/src/assets/samples/vpn-onboard-2.json +30 -30
- package/src/assets/samples/vpn-onboard-3.json +27 -27
- package/src/assets/samples/vpn-onboard-4.json +27 -27
- package/src/assets/samples/vpn-onboard-5.json +40 -40
- package/src/assets/samples/vpn-onboard-6.json +30 -30
- package/src/assets/samples/vpn-onboard-7.json +29 -29
- package/src/attribute-analyser/style/web/useExtractImageStyle.ts +8 -3
- package/src/attribute-analyser/style/web/useExtractViewStyle.ts +8 -3
- package/src/attributes-editor/AttributesEditorView.tsx +17 -6
- package/src/attributes-editor/FallbackLocalizationField.tsx +384 -0
- package/src/build-components/CarouselDots/CarouselDots.tsx +8 -3
- package/src/build-components/Main/Main.tsx +3 -1
- package/src/build-components/NavigationBarColor/NavigationBarColor.tsx +15 -1
- package/src/build-components/NavigationBarColor/NavigationBarColorProps.generated.ts +1 -52
- package/src/build-components/NavigationBarColor/pattern.json +11 -2
- package/src/build-components/OnboardDot/OnboardDot.tsx +3 -2
- package/src/build-components/PaywallCloseButton/pattern.json +1 -0
- package/src/build-components/StatusBarColor/StatusBarColor.tsx +15 -1
- package/src/build-components/StatusBarColor/StatusBarColorProps.generated.ts +1 -1
- package/src/build-components/StatusBarColor/pattern.json +10 -1
- package/src/build-components/patterns.generated.ts +25 -364
- package/src/components/BottomBar.tsx +135 -31
- package/src/components/BuilderProvider.tsx +1 -0
- package/src/components/DeviceButton.tsx +35 -0
- package/src/components/EditorHeader.tsx +16 -1
- package/src/hooks/useLocalize.ts +3 -1
- package/src/hooks/useSafeAreaViewStyle.ts +24 -4
- package/src/index.web.ts +19 -0
- package/src/mockOS/context/MockOSContext.tsx +41 -13
- package/src/modals/DeviceSelectorModal.tsx +94 -10
- package/src/modals/InspectModal.tsx +112 -4
- package/src/product-base/buildPaywallLocalizationParams.ts +3 -0
- package/src/product-base/extractAndroidParams.ts +38 -8
- package/src/product-base/types.ts +3 -0
- package/src/size-matters/index.ts +15 -9
- package/src/store.ts +66 -0
- package/src/styles/modals/_product-edit-modal.scss +2 -2
- package/src/types/Device.ts +5 -0
- package/src/types/PreviewConfig.ts +6 -0
- package/src/utils/analyseNodeByPatterns.ts +6 -2
- package/src/utils/extractTextStyle/extractTextStyle.ts +3 -1
- package/src/utils/extractTextStyle/extractTextStyleNative.ts +1 -1
- package/src/utils/extractViewStyle/extractViewStyle.ts +19 -5
- package/src/utils/extractViewStyle/extractViewStyleNative.ts +5 -1
- 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
|
-
{
|
|
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
|
package/src/hooks/useLocalize.ts
CHANGED
|
@@ -23,7 +23,9 @@ export function useLocalize(options?: {
|
|
|
23
23
|
|
|
24
24
|
return useCallback(
|
|
25
25
|
(keyOrText: string) => {
|
|
26
|
-
const
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
//
|
|
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
|
-
}, [
|
|
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
|
-
<
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
'
|
|
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
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 {
|