@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
@@ -116,6 +116,8 @@ export interface Product {
116
116
  displayPrice?: string;
117
117
  localizedPrice?: string;
118
118
  price?: string;
119
+ promoPrice?: string;
120
+ localizedPromoPrice?: string;
119
121
  currency?: string;
120
122
  currencyCode?: string;
121
123
  platform?: 'android' | 'ios';
@@ -169,6 +171,7 @@ export interface ProductParams {
169
171
  productCurreny: string;
170
172
  productId: string;
171
173
  productSelected: string;
174
+ subscribe: string;
172
175
  }
173
176
 
174
177
  /** Keys excluded from single (per-option) params — these are global/context-level. */
@@ -52,15 +52,21 @@ export const ms = (size: number, factor?: number) =>
52
52
 
53
53
  export function parseSize(
54
54
  value: string | number | undefined,
55
- baseSize?: BaseSize,
56
- device?: Device,
55
+ baseSize: BaseSize | undefined,
56
+ device: Device | 'native' | undefined,
57
57
  ) {
58
58
  if (value === undefined) return undefined;
59
+
60
+ const multiplier = device === 'native' ? 1 : (device?.multiplier ?? 1);
61
+
59
62
  if (typeof value === 'number') {
60
- return value;
63
+ return value * multiplier;
61
64
  }
62
65
 
63
- const scalers = getScalers(baseSize, device);
66
+ const scalers = getScalers(
67
+ baseSize,
68
+ device === 'native' ? undefined : device,
69
+ );
64
70
 
65
71
  const raw = String(value).trim();
66
72
  const lower = raw.toLowerCase();
@@ -68,16 +74,16 @@ export function parseSize(
68
74
  // Handle explicit scalers via suffixes
69
75
  if (lower.endsWith('@s')) {
70
76
  const n = parseFloat(lower.slice(0, -2));
71
- return Number.isFinite(n) ? scalers.scale(n) : raw;
77
+ return Number.isFinite(n) ? scalers.scale(n) * multiplier : raw;
72
78
  }
73
79
  if (lower.endsWith('@vs')) {
74
80
  const n = parseFloat(lower.slice(0, -3));
75
- return Number.isFinite(n) ? scalers.verticalScale(n) : raw;
81
+ return Number.isFinite(n) ? scalers.verticalScale(n) * multiplier : raw;
76
82
  }
77
83
  if (lower.endsWith('@f') || lower.endsWith('@fs')) {
78
84
  const cut = lower.endsWith('@f') ? -2 : -3;
79
85
  const n = parseFloat(lower.slice(0, cut));
80
- return Number.isFinite(n) ? scalers.verticalScale(n) : raw;
86
+ return Number.isFinite(n) ? scalers.verticalScale(n) * multiplier : raw;
81
87
  }
82
88
 
83
89
  // Preserve percentage values as-is
@@ -89,13 +95,13 @@ export function parseSize(
89
95
  // Handle px explicitly (treat as absolute, not scaled)
90
96
  if (lower.endsWith('px')) {
91
97
  const n = parseFloat(lower.replace('px', ''));
92
- return Number.isFinite(n) ? n : raw;
98
+ return Number.isFinite(n) ? n * multiplier : raw;
93
99
  }
94
100
 
95
101
  // Plain numeric strings fall back to provided scaler (implicit number)
96
102
  const numeric = parseFloat(lower);
97
103
  if (Number.isFinite(numeric)) {
98
- return numeric;
104
+ return numeric * multiplier;
99
105
  }
100
106
 
101
107
  // Unknown format, return as-is
package/src/store.ts CHANGED
@@ -1,3 +1,18 @@
1
+ import type { ComponentType } from 'react';
2
+
3
+ export type LocalizationApiConfig = {
4
+ forgeUrl: string;
5
+ forgeToken: string;
6
+ forgeAppId?: string | number | null;
7
+ };
8
+
9
+ export type LanguageColumn = {
10
+ id: number;
11
+ code: string;
12
+ title: string;
13
+ iso_code: string;
14
+ is_right_alignment: boolean;
15
+ };
1
16
  import { createWithEqualityFn } from 'zustand/traditional';
2
17
  import { shallow } from 'zustand/shallow';
3
18
  import type { Device } from './types/Device';
@@ -43,6 +58,8 @@ type RenderStore = {
43
58
  setLocalization: (localization: Localication) => void;
44
59
  isRtl: boolean;
45
60
  setIsRtl: (isRtl: boolean) => void;
61
+ languageColumns: LanguageColumn[];
62
+ setLanguageColumns: (cols: LanguageColumn[]) => void;
46
63
  previewMode: boolean;
47
64
  setPreviewMode: (previewMode: boolean) => void;
48
65
  // Mockable: products (persisted)
@@ -92,8 +109,27 @@ type RenderStore = {
92
109
  // OS bar color overrides (set by StatusBarColor / NavigationBarColor build components)
93
110
  statusBarOverrideColor: string | null;
94
111
  setStatusBarOverrideColor: (color: string | null) => void;
112
+ statusBarOverrideTranslucent: boolean | null;
113
+ setStatusBarOverrideTranslucent: (translucent: boolean | null) => void;
95
114
  navBarOverrideColor: string | null;
96
115
  setNavBarOverrideColor: (color: string | null) => void;
116
+ navBarOverrideTranslucent: boolean | null;
117
+ setNavBarOverrideTranslucent: (translucent: boolean | null) => void;
118
+
119
+ favoriteDevices: string[];
120
+ toggleFavoriteDevice: (name: string) => void;
121
+
122
+ // Host-provided custom renderer for the "Text" (string children) field
123
+ renderStringChildrenField?: ComponentType<{
124
+ value: string;
125
+ onChange: (v: string) => void;
126
+ }>;
127
+ setRenderStringChildrenField: (
128
+ comp?: ComponentType<{ value: string; onChange: (v: string) => void }>,
129
+ ) => void;
130
+
131
+ localizationApiConfig?: LocalizationApiConfig;
132
+ setLocalizationApiConfig: (config?: LocalizationApiConfig) => void;
97
133
  };
98
134
 
99
135
  export const useRenderStore = createWithEqualityFn<RenderStore>()(
@@ -122,6 +158,8 @@ export const useRenderStore = createWithEqualityFn<RenderStore>()(
122
158
  setLocalization: (localization) => set({ localization }),
123
159
  isRtl: false,
124
160
  setIsRtl: (isRtl) => set({ isRtl }),
161
+ languageColumns: [],
162
+ setLanguageColumns: (cols) => set({ languageColumns: cols }),
125
163
  previewMode: false,
126
164
  setPreviewMode: (previewMode) => set({ previewMode }),
127
165
  products: [],
@@ -269,8 +307,34 @@ export const useRenderStore = createWithEqualityFn<RenderStore>()(
269
307
  statusBarOverrideColor: null,
270
308
  setStatusBarOverrideColor: (color) =>
271
309
  set({ statusBarOverrideColor: color }),
310
+ statusBarOverrideTranslucent: null,
311
+ setStatusBarOverrideTranslucent: (translucent) =>
312
+ set({ statusBarOverrideTranslucent: translucent }),
272
313
  navBarOverrideColor: null,
273
314
  setNavBarOverrideColor: (color) => set({ navBarOverrideColor: color }),
315
+ navBarOverrideTranslucent: null,
316
+ setNavBarOverrideTranslucent: (translucent) =>
317
+ set({ navBarOverrideTranslucent: translucent }),
318
+
319
+ favoriteDevices: [],
320
+ toggleFavoriteDevice: (name) =>
321
+ set((state) => {
322
+ const devs = state.favoriteDevices || [];
323
+ const isFav = devs.includes(name);
324
+ return {
325
+ favoriteDevices: isFav
326
+ ? devs.filter((n) => n !== name)
327
+ : [...devs, name],
328
+ };
329
+ }),
330
+
331
+ renderStringChildrenField: undefined,
332
+ setRenderStringChildrenField: (comp) =>
333
+ set({ renderStringChildrenField: comp }),
334
+
335
+ localizationApiConfig: undefined,
336
+ setLocalizationApiConfig: (config) =>
337
+ set({ localizationApiConfig: config }),
274
338
  }),
275
339
  {
276
340
  name: 'render-store',
@@ -281,6 +345,8 @@ export const useRenderStore = createWithEqualityFn<RenderStore>()(
281
345
  products: state.products,
282
346
  benefits: state.benefits,
283
347
  listMaxNested: state.listMaxNested,
348
+ favoriteDevices: state.favoriteDevices,
349
+ device: state.device,
284
350
  }),
285
351
  storage: createJSONStorage(() => localStorage),
286
352
  },
@@ -1,7 +1,7 @@
1
1
  @use '../foundation/sizes' as sizes;
2
2
 
3
3
  .product-edit-modal {
4
- width: min(640px, calc(100vw - 32px));
4
+ width: min(800px, calc(100vw - 32px));
5
5
  max-height: 90vh;
6
6
  padding: sizes.$spaceRoomy;
7
7
  display: flex;
@@ -17,7 +17,7 @@
17
17
  }
18
18
 
19
19
  .product-edit-modal__textarea {
20
- min-height: 90px;
20
+ min-height: 140px;
21
21
  padding: 8px;
22
22
  resize: vertical;
23
23
  }
@@ -35,4 +35,9 @@ export interface Device {
35
35
  * 1 = highest importance, 100 = lowest importance
36
36
  */
37
37
  importance?: number;
38
+ /**
39
+ * Optional multiplier to adjust for differences in CSS viewport sizing between web and native.
40
+ * Example: 0.5 for a device where web renders it twice as large as expected compared to native.
41
+ */
42
+ multiplier?: number;
38
43
  }
@@ -15,6 +15,8 @@ export type LocalizationKey =
15
15
  | 'base.builder.paywall.promo.default.text'
16
16
  | 'base.builder.paywall.promo.freeTrial.text'
17
17
  | 'base.builder.paywall.promo.regular.text'
18
+ | 'base.builder.paywall.button.subscribe'
19
+ | 'base.builder.paywall.button.subscribe-free'
18
20
  // onboard – titles
19
21
  | 'base.onboard.title.one-page'
20
22
  | 'base.onboard.title.two-page'
@@ -79,6 +81,8 @@ export const defaultLocalization: Localication = {
79
81
  '@trialPeriod-@trialPeriodUnit free trial',
80
82
  'base.builder.paywall.promo.regular.text':
81
83
  '@localizedPrice @localizedPeriod',
84
+ 'base.builder.paywall.button.subscribe': 'Subscribe',
85
+ 'base.builder.paywall.button.subscribe-free': 'Subscribe with Free Trial',
82
86
  // onboard – titles
83
87
  'base.onboard.title.one-page': 'Secure your connection',
84
88
  'base.onboard.title.two-page': 'Access content worldwide',
@@ -146,6 +150,8 @@ export const defaultLocalization: Localication = {
146
150
  '@trialPeriod @trialPeriodUnit ücretsiz deneme',
147
151
  'base.builder.paywall.promo.regular.text':
148
152
  '@localizedPrice @localizedPeriod',
153
+ 'base.builder.paywall.button.subscribe': 'Abone Ol',
154
+ 'base.builder.paywall.button.subscribe-free': 'Ücretsiz Denemeyi Başlat',
149
155
  // onboard – titles
150
156
  'base.onboard.title.five-page': 'Deneyiminizi geliştirin',
151
157
  'base.onboard.title.six-page': 'Her şey hazır!',
@@ -429,11 +429,15 @@ function validateAttributesByPattern(
429
429
  if (attrName === 'style' || attrName === 'styles') continue;
430
430
  const attrSpec = schema?.[attrName] as AttributeTypeSpec | undefined;
431
431
  if (!attrSpec) {
432
- if (attrName === 'title' || attrName === 'description') {
432
+ if (
433
+ attrName === 'title' ||
434
+ attrName === 'description' ||
435
+ attrName === 'testID'
436
+ ) {
433
437
  const res = validateAttributeValue(
434
438
  pattern.pattern.type,
435
439
  attrValue,
436
- attrName,
440
+ attrName === 'testID' ? 'string' : attrName,
437
441
  joinPath(path, attrName),
438
442
  );
439
443
  if (!res.valid) return res;
@@ -95,6 +95,7 @@ export type ExtractTextStyleOptions = {
95
95
  onError?: (error: string) => void;
96
96
  directlyTextStyle?: boolean;
97
97
  baseSize?: BaseSize;
98
+ device?: import('../../types/Device').Device;
98
99
  };
99
100
 
100
101
  export function extractTextStyle<T extends TextPropsGenerated['attributes']>(
@@ -127,7 +128,7 @@ export function extractTextStyle<T extends TextPropsGenerated['attributes']>(
127
128
  // Typography
128
129
  const fontSize = get('fontSize') as string | number | undefined;
129
130
  if (fontSize !== undefined) {
130
- const parsed = parseSize(fontSize, options.baseSize);
131
+ const parsed = parseSize(fontSize, options.baseSize, options.device!);
131
132
  style.fontSize = parsed as React.CSSProperties['fontSize'];
132
133
  } else {
133
134
  style.fontSize = fs(14);
@@ -194,6 +195,7 @@ export function extractTextStyle<T extends TextPropsGenerated['attributes']>(
194
195
  projectColors: options.projectColors,
195
196
  theme,
196
197
  baseSize: options.baseSize,
198
+ device: options.device,
197
199
  });
198
200
  const fullStyle = { ...viewStyle, ...style };
199
201
 
@@ -63,7 +63,7 @@ export function extractTextStyleNative<
63
63
  }
64
64
 
65
65
  const rawFontSize = get('fontSize') as string | number | undefined;
66
- const parsedFontSize = parseSize(rawFontSize, options.baseSize);
66
+ const parsedFontSize = parseSize(rawFontSize, options.baseSize, 'native');
67
67
  if (typeof parsedFontSize === 'number') style.fontSize = parsedFontSize;
68
68
  else style.fontSize = fs(14);
69
69
 
@@ -8,11 +8,13 @@ import type { BaseSize } from '../../types/PreviewConfig';
8
8
  import { parseSize } from '../../size-matters';
9
9
  import { parseColor } from '../parseColor';
10
10
  import { getStyleBag, toAttributeRecord } from '../attributeStyle';
11
+ import { getDefaultDevice } from '../getDevices';
11
12
 
12
13
  export type ExtractViewStyleOptions = {
13
14
  projectColors?: ProjectColors;
14
15
  theme?: string;
15
16
  baseSize?: BaseSize;
17
+ device?: import('../../types/Device').Device;
16
18
  };
17
19
 
18
20
  export function extractViewStyle<T extends ViewPropsGenerated['attributes']>(
@@ -74,7 +76,7 @@ export function extractViewStyle<T extends ViewPropsGenerated['attributes']>(
74
76
  rawValue: string | number | undefined,
75
77
  ) => {
76
78
  if (isEmptySizeValue(rawValue)) return;
77
- const parsed = parseSize(rawValue, options.baseSize);
79
+ const parsed = parseSize(rawValue, options.baseSize, options.device!);
78
80
  style[property] = parsed as React.CSSProperties[K];
79
81
  };
80
82
 
@@ -87,13 +89,21 @@ export function extractViewStyle<T extends ViewPropsGenerated['attributes']>(
87
89
  | number
88
90
  | undefined;
89
91
  if (!isEmptySizeValue(paddingHorizontal)) {
90
- const parsed = parseSize(paddingHorizontal, options.baseSize);
92
+ const parsed = parseSize(
93
+ paddingHorizontal,
94
+ options.baseSize,
95
+ options.device!,
96
+ );
91
97
  style.paddingLeft = parsed as React.CSSProperties['paddingLeft'];
92
98
  style.paddingRight = parsed as React.CSSProperties['paddingRight'];
93
99
  }
94
100
  const paddingVertical = get('paddingVertical') as string | number | undefined;
95
101
  if (!isEmptySizeValue(paddingVertical)) {
96
- const parsed = parseSize(paddingVertical, options.baseSize);
102
+ const parsed = parseSize(
103
+ paddingVertical,
104
+ options.baseSize,
105
+ options.device!,
106
+ );
97
107
  style.paddingTop = parsed as React.CSSProperties['paddingTop'];
98
108
  style.paddingBottom = parsed as React.CSSProperties['paddingBottom'];
99
109
  }
@@ -117,14 +127,18 @@ export function extractViewStyle<T extends ViewPropsGenerated['attributes']>(
117
127
  (attrRecord.marginHorizontal as string | number | undefined) ??
118
128
  (styleBag?.marginHorizontal as string | number | undefined);
119
129
  if (!isEmptySizeValue(marginHorizontalRaw)) {
120
- const parsed = parseSize(marginHorizontalRaw, options.baseSize);
130
+ const parsed = parseSize(
131
+ marginHorizontalRaw,
132
+ options.baseSize,
133
+ options.device!,
134
+ );
121
135
  style.marginLeft = parsed as React.CSSProperties['marginLeft'];
122
136
  style.marginRight = parsed as React.CSSProperties['marginRight'];
123
137
  }
124
138
 
125
139
  const marginVertical = get('marginVertical') as string | number | undefined;
126
140
  if (!isEmptySizeValue(marginVertical)) {
127
- const parsed = parseSize(marginVertical, options.baseSize);
141
+ const parsed = parseSize(marginVertical, options.baseSize, options.device!);
128
142
  style.marginTop = parsed as React.CSSProperties['marginTop'];
129
143
  style.marginBottom = parsed as React.CSSProperties['marginBottom'];
130
144
  }
@@ -51,7 +51,11 @@ export function extractViewStyleNative<
51
51
 
52
52
  const setParsedSize = (property: string, rawValue: unknown) => {
53
53
  if (isEmptySizeValue(rawValue)) return;
54
- const parsed = parseSize(rawValue as string | number, options.baseSize);
54
+ const parsed = parseSize(
55
+ rawValue as string | number,
56
+ options.baseSize,
57
+ 'native',
58
+ );
55
59
  // RN generally expects numbers (dp). We allow percentages for width/height-like props.
56
60
  if (typeof parsed === 'number' || typeof parsed === 'string') {
57
61
  style[property] = parsed;
@@ -9,11 +9,9 @@ export function replaceLocalizationParams(
9
9
  // (e.g. @trialPeriod matching inside @trialPeriodUnit)
10
10
  const sorted = Object.entries(params).sort(([a], [b]) => b.length - a.length);
11
11
 
12
- return (
13
- sorted.reduce((acc, [key, val]) => {
14
- if (val == null) return acc;
15
- const needle = `@${key}`;
16
- return acc.split(needle).join(val);
17
- }, text) || text
18
- );
12
+ return sorted.reduce((acc, [key, val]) => {
13
+ if (val == null) return acc;
14
+ const needle = `@${key}`;
15
+ return acc.split(needle).join(val);
16
+ }, text);
19
17
  }