Skip to content

Commit 93232f5

Browse files
authored
Merge pull request #858 from Iterable/loren/embedded/SDK-433-json-parser-crashes-with-incorrect-configs
[SDK-433] JSON parse error when config contains unexpected type
2 parents 7ac4c6f + 1526040 commit 93232f5

3 files changed

Lines changed: 189 additions & 2 deletions

File tree

src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Iterable } from '../../../core/classes/Iterable';
33
import { IterableEmbeddedViewType } from '../../enums';
44
import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedComponentProps';
55
import type { IterableEmbeddedMessageElementsButton } from '../../types/IterableEmbeddedMessageElementsButton';
6+
import { normalizeEmbeddedViewConfig } from '../../utils/normalizeEmbeddedViewConfig';
67
import { getMedia } from './getMedia';
78
import { getStyles } from './getStyles';
89

@@ -44,9 +45,14 @@ export const useEmbeddedView = (
4445
onMessageClick = noop,
4546
}: IterableEmbeddedComponentProps
4647
) => {
48+
const normalizedConfig = useMemo(
49+
() => normalizeEmbeddedViewConfig(config),
50+
[config]
51+
);
52+
4753
const parsedStyles = useMemo(() => {
48-
return getStyles(viewType, config);
49-
}, [viewType, config]);
54+
return getStyles(viewType, normalizedConfig);
55+
}, [viewType, normalizedConfig]);
5056
const media = useMemo(() => {
5157
return getMedia(viewType, message);
5258
}, [viewType, message]);
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { normalizeEmbeddedViewConfig } from './normalizeEmbeddedViewConfig';
2+
3+
describe('normalizeEmbeddedViewConfig', () => {
4+
let warnSpy: jest.SpyInstance;
5+
6+
beforeEach(() => {
7+
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
8+
});
9+
10+
afterEach(() => {
11+
warnSpy.mockRestore();
12+
});
13+
14+
it('returns null or undefined unchanged', () => {
15+
expect(normalizeEmbeddedViewConfig(null)).toBeNull();
16+
expect(normalizeEmbeddedViewConfig(undefined)).toBeUndefined();
17+
});
18+
19+
it('parses numeric strings for borderWidth and borderCornerRadius', () => {
20+
const input = {
21+
borderWidth: '45',
22+
borderCornerRadius: '12.5',
23+
backgroundColor: '#fff',
24+
};
25+
26+
// Runtime JSON / native payloads may use strings for numeric fields.
27+
const result = normalizeEmbeddedViewConfig(input as never);
28+
29+
expect(result).toEqual({
30+
borderWidth: 45,
31+
borderCornerRadius: 12.5,
32+
backgroundColor: '#fff',
33+
});
34+
expect(warnSpy).not.toHaveBeenCalled();
35+
});
36+
37+
it('trims whitespace before parsing numeric strings', () => {
38+
const result = normalizeEmbeddedViewConfig({
39+
borderWidth: ' 8 ',
40+
} as never);
41+
42+
expect(result?.borderWidth).toBe(8);
43+
expect(warnSpy).not.toHaveBeenCalled();
44+
});
45+
46+
it('leaves valid numbers unchanged', () => {
47+
const result = normalizeEmbeddedViewConfig({
48+
borderWidth: 3,
49+
borderCornerRadius: 0,
50+
});
51+
52+
expect(result?.borderWidth).toBe(3);
53+
expect(result?.borderCornerRadius).toBe(0);
54+
expect(warnSpy).not.toHaveBeenCalled();
55+
});
56+
57+
it('drops non-parsable strings and warns', () => {
58+
const result = normalizeEmbeddedViewConfig({
59+
borderWidth: 'nope',
60+
borderCornerRadius: '10',
61+
} as never);
62+
63+
expect(result?.borderWidth).toBeUndefined();
64+
expect(result?.borderCornerRadius).toBe(10);
65+
expect(warnSpy).toHaveBeenCalledTimes(1);
66+
expect(warnSpy.mock.calls[0][0]).toContain('borderWidth');
67+
});
68+
69+
it('drops empty strings and warns', () => {
70+
const result = normalizeEmbeddedViewConfig({
71+
borderWidth: ' ',
72+
} as never);
73+
74+
expect(result?.borderWidth).toBeUndefined();
75+
expect(warnSpy).toHaveBeenCalled();
76+
});
77+
78+
it('drops NaN and Infinity numbers and warns', () => {
79+
const result = normalizeEmbeddedViewConfig({
80+
borderWidth: Number.NaN,
81+
borderCornerRadius: Number.POSITIVE_INFINITY,
82+
} as never);
83+
84+
expect(result?.borderWidth).toBeUndefined();
85+
expect(result?.borderCornerRadius).toBeUndefined();
86+
expect(warnSpy).toHaveBeenCalledTimes(2);
87+
});
88+
89+
it('drops invalid types and warns', () => {
90+
const result = normalizeEmbeddedViewConfig({
91+
borderWidth: true,
92+
} as never);
93+
94+
expect(result?.borderWidth).toBeUndefined();
95+
expect(warnSpy).toHaveBeenCalled();
96+
});
97+
98+
it('does not mutate the original config object', () => {
99+
const original = { borderWidth: '7' as const };
100+
const snapshot = { ...original };
101+
102+
normalizeEmbeddedViewConfig(original as never);
103+
104+
expect(original).toEqual(snapshot);
105+
});
106+
});
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type { IterableEmbeddedViewConfig } from '../types/IterableEmbeddedViewConfig';
2+
3+
const NUMERIC_KEYS: (keyof Pick<
4+
IterableEmbeddedViewConfig,
5+
'borderWidth' | 'borderCornerRadius'
6+
>)[] = ['borderWidth', 'borderCornerRadius'];
7+
8+
function coerceNumericField(
9+
key: 'borderWidth' | 'borderCornerRadius',
10+
value: unknown
11+
): number | undefined {
12+
if (value === undefined || value === null) {
13+
return undefined;
14+
}
15+
if (typeof value === 'number') {
16+
if (Number.isFinite(value)) {
17+
return value;
18+
}
19+
console.warn(
20+
`[IterableEmbeddedView] Ignoring ${String(key)}: expected a finite number, got ${String(value)}`
21+
);
22+
return undefined;
23+
}
24+
if (typeof value === 'string') {
25+
const trimmed = value.trim();
26+
if (trimmed === '') {
27+
console.warn(
28+
`[IterableEmbeddedView] Ignoring ${String(key)}: empty string is not a valid number`
29+
);
30+
return undefined;
31+
}
32+
const n = parseFloat(trimmed);
33+
if (Number.isFinite(n)) {
34+
return n;
35+
}
36+
console.warn(
37+
`[IterableEmbeddedView] Ignoring ${String(key)}: could not parse string as a number: ${JSON.stringify(value)}`
38+
);
39+
return undefined;
40+
}
41+
console.warn(
42+
`[IterableEmbeddedView] Ignoring ${String(key)}: expected number or numeric string, got ${typeof value}`
43+
);
44+
return undefined;
45+
}
46+
47+
/**
48+
* Returns a shallow copy of config with numeric fields coerced from strings when possible.
49+
* Values that cannot be coerced are omitted so style resolution can fall back to defaults.
50+
*/
51+
export function normalizeEmbeddedViewConfig(
52+
config: IterableEmbeddedViewConfig | null | undefined
53+
): IterableEmbeddedViewConfig | null | undefined {
54+
if (config == null) {
55+
return config;
56+
}
57+
const next: IterableEmbeddedViewConfig = { ...config };
58+
const loose = config as Record<string, unknown>;
59+
for (const key of NUMERIC_KEYS) {
60+
const raw = loose[key as string];
61+
if (raw === undefined) {
62+
continue;
63+
}
64+
if (typeof raw === 'number' && Number.isFinite(raw)) {
65+
continue;
66+
}
67+
const coerced = coerceNumericField(key, raw);
68+
if (coerced === undefined) {
69+
delete next[key];
70+
} else {
71+
next[key] = coerced;
72+
}
73+
}
74+
return next;
75+
}

0 commit comments

Comments
 (0)