flowboard-react 0.3.0 → 0.4.1
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/README.md +12 -0
- package/lib/module/Flowboard.js +47 -9
- package/lib/module/Flowboard.js.map +1 -1
- package/lib/module/components/FlowboardRenderer.js +168 -17
- package/lib/module/components/FlowboardRenderer.js.map +1 -1
- package/lib/module/core/resolverService.js +31 -1
- package/lib/module/core/resolverService.js.map +1 -1
- package/lib/typescript/src/Flowboard.d.ts +5 -1
- package/lib/typescript/src/Flowboard.d.ts.map +1 -1
- package/lib/typescript/src/components/FlowboardRenderer.d.ts.map +1 -1
- package/lib/typescript/src/core/resolverService.d.ts +6 -0
- package/lib/typescript/src/core/resolverService.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types/flowboard.d.ts +3 -0
- package/lib/typescript/src/types/flowboard.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Flowboard.ts +81 -14
- package/src/components/FlowboardRenderer.tsx +199 -24
- package/src/core/resolverService.ts +42 -2
- package/src/index.tsx +1 -0
- package/src/types/flowboard.ts +4 -0
package/src/Flowboard.ts
CHANGED
|
@@ -3,7 +3,11 @@ import { ClientContext } from './core/clientContext';
|
|
|
3
3
|
import { ResolverService } from './core/resolverService';
|
|
4
4
|
import { OnboardingRepository } from './core/onboardingRepository';
|
|
5
5
|
import { AnalyticsManager } from './core/analyticsManager';
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
FlowboardData,
|
|
8
|
+
FlowboardLaunchByIdOptions,
|
|
9
|
+
FlowboardLaunchOptions,
|
|
10
|
+
} from './types/flowboard';
|
|
7
11
|
|
|
8
12
|
export type LaunchPayload = {
|
|
9
13
|
data: FlowboardData;
|
|
@@ -72,19 +76,7 @@ export class Flowboard {
|
|
|
72
76
|
);
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
|
|
76
|
-
const context =
|
|
77
|
-
AnalyticsManager.instance.clientContextSnapshot ??
|
|
78
|
-
(await ClientContext.create());
|
|
79
|
-
AnalyticsManager.instance.configure({
|
|
80
|
-
enabled: options.enableAnalytics ?? true,
|
|
81
|
-
context,
|
|
82
|
-
});
|
|
83
|
-
} catch (error) {
|
|
84
|
-
Flowboard.log(
|
|
85
|
-
`Failed to configure analytics for launch: ${String(error)}`
|
|
86
|
-
);
|
|
87
|
-
}
|
|
79
|
+
await Flowboard.configureAnalyticsForLaunch(options.enableAnalytics);
|
|
88
80
|
|
|
89
81
|
let data = await Flowboard.repository.getOnboardingJson();
|
|
90
82
|
|
|
@@ -117,6 +109,48 @@ export class Flowboard {
|
|
|
117
109
|
return;
|
|
118
110
|
}
|
|
119
111
|
|
|
112
|
+
await Flowboard.launchResolvedOnboarding(data, options);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
static async launchOnboardingById(
|
|
116
|
+
onboardingId: string,
|
|
117
|
+
options: FlowboardLaunchByIdOptions = {}
|
|
118
|
+
): Promise<void> {
|
|
119
|
+
Flowboard.log(`Attempting to launch onboarding by id: ${onboardingId}`);
|
|
120
|
+
if (!Flowboard.appId) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
'Flowboard.init() must be called before launchOnboardingById'
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const normalizedOnboardingId = onboardingId.trim();
|
|
127
|
+
if (!normalizedOnboardingId) {
|
|
128
|
+
throw new Error('onboardingId must be a non-empty string');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await Flowboard.configureAnalyticsForLaunch(options.enableAnalytics);
|
|
132
|
+
|
|
133
|
+
const locale = await Flowboard.resolveLocale(options.locale);
|
|
134
|
+
|
|
135
|
+
let data: FlowboardData;
|
|
136
|
+
try {
|
|
137
|
+
data = await Flowboard.service.fetchOnboardingById({
|
|
138
|
+
onboardingId: normalizedOnboardingId,
|
|
139
|
+
locale,
|
|
140
|
+
});
|
|
141
|
+
} catch (error) {
|
|
142
|
+
Flowboard.log(`Failed to load onboarding by id: ${String(error)}`);
|
|
143
|
+
Alert.alert('Failed to load onboarding', String(error));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await Flowboard.launchResolvedOnboarding(data, options);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private static async launchResolvedOnboarding(
|
|
151
|
+
data: FlowboardData,
|
|
152
|
+
options: FlowboardLaunchOptions
|
|
153
|
+
): Promise<void> {
|
|
120
154
|
const shouldResumeProgress =
|
|
121
155
|
options.resumeProgress === true && options.alwaysRestart !== true;
|
|
122
156
|
|
|
@@ -159,6 +193,39 @@ export class Flowboard {
|
|
|
159
193
|
});
|
|
160
194
|
}
|
|
161
195
|
|
|
196
|
+
private static async configureAnalyticsForLaunch(
|
|
197
|
+
enableAnalytics: boolean | undefined
|
|
198
|
+
): Promise<void> {
|
|
199
|
+
try {
|
|
200
|
+
const context =
|
|
201
|
+
AnalyticsManager.instance.clientContextSnapshot ??
|
|
202
|
+
(await ClientContext.create());
|
|
203
|
+
AnalyticsManager.instance.configure({
|
|
204
|
+
enabled: enableAnalytics ?? true,
|
|
205
|
+
context,
|
|
206
|
+
});
|
|
207
|
+
} catch (error) {
|
|
208
|
+
Flowboard.log(
|
|
209
|
+
`Failed to configure analytics for launch: ${String(error)}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private static async resolveLocale(
|
|
215
|
+
requestedLocale: string | undefined
|
|
216
|
+
): Promise<string> {
|
|
217
|
+
const normalized = requestedLocale?.trim();
|
|
218
|
+
if (normalized) {
|
|
219
|
+
return normalized;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const context =
|
|
223
|
+
AnalyticsManager.instance.clientContextSnapshot ??
|
|
224
|
+
(await ClientContext.create());
|
|
225
|
+
|
|
226
|
+
return context.locale || 'en_US';
|
|
227
|
+
}
|
|
228
|
+
|
|
162
229
|
private static async initialize(): Promise<void> {
|
|
163
230
|
Flowboard.log('Checking local cache...');
|
|
164
231
|
const cached = await Flowboard.repository.getOnboardingJson();
|
|
@@ -1594,6 +1594,9 @@ function WheelPicker({
|
|
|
1594
1594
|
}),
|
|
1595
1595
|
[formData, itemTemplate, options, properties]
|
|
1596
1596
|
);
|
|
1597
|
+
const wheelScrollY = React.useRef(new Animated.Value(0)).current;
|
|
1598
|
+
const wheelScrollRef = React.useRef<any>(null);
|
|
1599
|
+
const lastCommittedWheelValue = React.useRef<string | null>(null);
|
|
1597
1600
|
|
|
1598
1601
|
const toggleSelection = (value: string) => {
|
|
1599
1602
|
setSelectedValues((prev) => {
|
|
@@ -1620,6 +1623,78 @@ function WheelPicker({
|
|
|
1620
1623
|
|
|
1621
1624
|
const layout = properties.layout ?? 'wheel';
|
|
1622
1625
|
const spacing = Number(properties.spacing ?? 8);
|
|
1626
|
+
const wheelItemHeight = Math.max(1, Number(properties.wheelItemHeight ?? 48));
|
|
1627
|
+
const requestedVisible = Math.max(
|
|
1628
|
+
3,
|
|
1629
|
+
Math.floor(Number(properties.visibleItems ?? 5))
|
|
1630
|
+
);
|
|
1631
|
+
const visibleItems =
|
|
1632
|
+
requestedVisible % 2 === 0 ? requestedVisible + 1 : requestedVisible;
|
|
1633
|
+
const wheelRowStride = wheelItemHeight + Math.max(0, spacing);
|
|
1634
|
+
const wheelContainerHeight = wheelItemHeight * visibleItems;
|
|
1635
|
+
|
|
1636
|
+
const clampWheelIndex = React.useCallback(
|
|
1637
|
+
(index: number) =>
|
|
1638
|
+
Math.max(0, Math.min(index, Math.max(0, resolvedOptions.length - 1))),
|
|
1639
|
+
[resolvedOptions.length]
|
|
1640
|
+
);
|
|
1641
|
+
|
|
1642
|
+
const getWheelSelectedIndex = React.useCallback(() => {
|
|
1643
|
+
if (resolvedOptions.length === 0) return 0;
|
|
1644
|
+
const targetValue = selectedValues[0];
|
|
1645
|
+
if (targetValue === undefined) return 0;
|
|
1646
|
+
const found = resolvedOptions.findIndex(
|
|
1647
|
+
(option) => String(option.value ?? '') === String(targetValue)
|
|
1648
|
+
);
|
|
1649
|
+
return found >= 0 ? found : 0;
|
|
1650
|
+
}, [resolvedOptions, selectedValues]);
|
|
1651
|
+
|
|
1652
|
+
const commitWheelSelection = React.useCallback(
|
|
1653
|
+
(index: number) => {
|
|
1654
|
+
const safeIndex = clampWheelIndex(index);
|
|
1655
|
+
const selectedOption = resolvedOptions[safeIndex];
|
|
1656
|
+
if (!selectedOption) return;
|
|
1657
|
+
const value = String(selectedOption.value ?? '');
|
|
1658
|
+
if (!value) return;
|
|
1659
|
+
|
|
1660
|
+
setSelectedValues((prev) => {
|
|
1661
|
+
if (prev.length === 1 && prev[0] === value) {
|
|
1662
|
+
return prev;
|
|
1663
|
+
}
|
|
1664
|
+
return [value];
|
|
1665
|
+
});
|
|
1666
|
+
|
|
1667
|
+
if (lastCommittedWheelValue.current !== value) {
|
|
1668
|
+
lastCommittedWheelValue.current = value;
|
|
1669
|
+
onChanged(value);
|
|
1670
|
+
if (properties.autoGoNext === true) {
|
|
1671
|
+
requestAnimationFrame(() =>
|
|
1672
|
+
onAction('next', { selectedValue: value })
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
},
|
|
1677
|
+
[clampWheelIndex, onAction, onChanged, properties.autoGoNext, resolvedOptions]
|
|
1678
|
+
);
|
|
1679
|
+
|
|
1680
|
+
React.useEffect(() => {
|
|
1681
|
+
if (layout !== 'wheel' || resolvedOptions.length === 0) return;
|
|
1682
|
+
const selectedIndex = getWheelSelectedIndex();
|
|
1683
|
+
const offsetY = selectedIndex * wheelRowStride;
|
|
1684
|
+
requestAnimationFrame(() => {
|
|
1685
|
+
wheelScrollRef.current?.scrollTo({
|
|
1686
|
+
y: offsetY,
|
|
1687
|
+
animated: false,
|
|
1688
|
+
});
|
|
1689
|
+
wheelScrollY.setValue(offsetY);
|
|
1690
|
+
});
|
|
1691
|
+
}, [
|
|
1692
|
+
getWheelSelectedIndex,
|
|
1693
|
+
layout,
|
|
1694
|
+
resolvedOptions.length,
|
|
1695
|
+
wheelRowStride,
|
|
1696
|
+
wheelScrollY,
|
|
1697
|
+
]);
|
|
1623
1698
|
|
|
1624
1699
|
const renderItem = (option: Record<string, any>, index: number) => {
|
|
1625
1700
|
const value = String(option.value ?? '');
|
|
@@ -1697,36 +1772,136 @@ function WheelPicker({
|
|
|
1697
1772
|
}
|
|
1698
1773
|
|
|
1699
1774
|
if (layout === 'wheel') {
|
|
1700
|
-
const
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1775
|
+
const centerPadding = Math.max(
|
|
1776
|
+
0,
|
|
1777
|
+
(wheelContainerHeight - wheelRowStride) / 2
|
|
1778
|
+
);
|
|
1779
|
+
const centerLineTop = (wheelContainerHeight - wheelItemHeight) / 2;
|
|
1780
|
+
const baseUnselectedStyle =
|
|
1781
|
+
properties.unselectedStyle ?? properties.selectedStyle ?? {};
|
|
1782
|
+
const overlayBorderColor = parseColor(
|
|
1783
|
+
properties.wheelCenterBorderColor ??
|
|
1784
|
+
properties.wheelSelectedBorderColor ??
|
|
1785
|
+
baseUnselectedStyle.borderColor ??
|
|
1786
|
+
'#D6D6DC'
|
|
1787
|
+
);
|
|
1788
|
+
const overlayBackgroundColor = parseColor(
|
|
1789
|
+
properties.wheelCenterBackgroundColor ?? '#24FFFFFF'
|
|
1704
1790
|
);
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1791
|
+
|
|
1792
|
+
const handleWheelMomentumEnd = (event: any) => {
|
|
1793
|
+
const offsetY = Number(event?.nativeEvent?.contentOffset?.y ?? 0);
|
|
1794
|
+
const settledIndex = clampWheelIndex(Math.round(offsetY / wheelRowStride));
|
|
1795
|
+
const targetOffset = settledIndex * wheelRowStride;
|
|
1796
|
+
if (Math.abs(targetOffset - offsetY) > 0.5) {
|
|
1797
|
+
wheelScrollRef.current?.scrollTo({
|
|
1798
|
+
y: targetOffset,
|
|
1799
|
+
animated: true,
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
commitWheelSelection(settledIndex);
|
|
1803
|
+
};
|
|
1709
1804
|
|
|
1710
1805
|
return (
|
|
1711
|
-
<View style={{ height:
|
|
1712
|
-
<ScrollView
|
|
1806
|
+
<View style={{ height: wheelContainerHeight, overflow: 'hidden' }}>
|
|
1807
|
+
<Animated.ScrollView
|
|
1808
|
+
ref={wheelScrollRef}
|
|
1713
1809
|
showsVerticalScrollIndicator={false}
|
|
1810
|
+
bounces={false}
|
|
1811
|
+
decelerationRate="fast"
|
|
1812
|
+
snapToInterval={wheelRowStride}
|
|
1813
|
+
snapToAlignment="start"
|
|
1814
|
+
disableIntervalMomentum={false}
|
|
1815
|
+
onMomentumScrollEnd={handleWheelMomentumEnd}
|
|
1714
1816
|
contentContainerStyle={{ paddingVertical: centerPadding }}
|
|
1817
|
+
onScroll={Animated.event(
|
|
1818
|
+
[{ nativeEvent: { contentOffset: { y: wheelScrollY } } }],
|
|
1819
|
+
{ useNativeDriver: true }
|
|
1820
|
+
)}
|
|
1821
|
+
scrollEventThrottle={16}
|
|
1715
1822
|
>
|
|
1716
|
-
{resolvedOptions.map((option, index) =>
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1823
|
+
{resolvedOptions.map((option, index) => {
|
|
1824
|
+
const inputRange = [
|
|
1825
|
+
(index - 2) * wheelRowStride,
|
|
1826
|
+
(index - 1) * wheelRowStride,
|
|
1827
|
+
index * wheelRowStride,
|
|
1828
|
+
(index + 1) * wheelRowStride,
|
|
1829
|
+
(index + 2) * wheelRowStride,
|
|
1830
|
+
];
|
|
1831
|
+
const opacity = wheelScrollY.interpolate({
|
|
1832
|
+
inputRange,
|
|
1833
|
+
outputRange: [0.35, 0.6, 1, 0.6, 0.35],
|
|
1834
|
+
extrapolate: 'clamp',
|
|
1835
|
+
});
|
|
1836
|
+
const scale = wheelScrollY.interpolate({
|
|
1837
|
+
inputRange,
|
|
1838
|
+
outputRange: [0.82, 0.91, 1, 0.91, 0.82],
|
|
1839
|
+
extrapolate: 'clamp',
|
|
1840
|
+
});
|
|
1841
|
+
const rotateX = wheelScrollY.interpolate({
|
|
1842
|
+
inputRange,
|
|
1843
|
+
outputRange: ['55deg', '28deg', '0deg', '-28deg', '-55deg'],
|
|
1844
|
+
extrapolate: 'clamp',
|
|
1845
|
+
});
|
|
1846
|
+
const translateY = wheelScrollY.interpolate({
|
|
1847
|
+
inputRange,
|
|
1848
|
+
outputRange: [-10, -4, 0, 4, 10],
|
|
1849
|
+
extrapolate: 'clamp',
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
return (
|
|
1853
|
+
<Animated.View
|
|
1854
|
+
key={`wheel-column-option-${index}`}
|
|
1855
|
+
style={{
|
|
1856
|
+
height: wheelRowStride,
|
|
1857
|
+
justifyContent: 'center',
|
|
1858
|
+
opacity,
|
|
1859
|
+
transform: [
|
|
1860
|
+
{ perspective: 1200 },
|
|
1861
|
+
{ translateY },
|
|
1862
|
+
{ rotateX },
|
|
1863
|
+
{ scale },
|
|
1864
|
+
],
|
|
1865
|
+
}}
|
|
1866
|
+
>
|
|
1867
|
+
<View style={{ minHeight: wheelItemHeight, justifyContent: 'center' }}>
|
|
1868
|
+
{renderItem(option, index)}
|
|
1869
|
+
</View>
|
|
1870
|
+
</Animated.View>
|
|
1871
|
+
);
|
|
1872
|
+
})}
|
|
1873
|
+
</Animated.ScrollView>
|
|
1874
|
+
|
|
1875
|
+
<View
|
|
1876
|
+
pointerEvents="none"
|
|
1877
|
+
style={{
|
|
1878
|
+
position: 'absolute',
|
|
1879
|
+
left: 0,
|
|
1880
|
+
right: 0,
|
|
1881
|
+
top: centerLineTop,
|
|
1882
|
+
height: wheelItemHeight,
|
|
1883
|
+
borderRadius: 14,
|
|
1884
|
+
borderWidth: 1,
|
|
1885
|
+
borderColor: overlayBorderColor,
|
|
1886
|
+
backgroundColor: overlayBackgroundColor,
|
|
1887
|
+
}}
|
|
1888
|
+
/>
|
|
1889
|
+
<LinearGradient
|
|
1890
|
+
pointerEvents="none"
|
|
1891
|
+
colors={['rgba(255,255,255,0.92)', 'rgba(255,255,255,0)']}
|
|
1892
|
+
style={{ position: 'absolute', top: 0, left: 0, right: 0, height: centerPadding }}
|
|
1893
|
+
/>
|
|
1894
|
+
<LinearGradient
|
|
1895
|
+
pointerEvents="none"
|
|
1896
|
+
colors={['rgba(255,255,255,0)', 'rgba(255,255,255,0.92)']}
|
|
1897
|
+
style={{
|
|
1898
|
+
position: 'absolute',
|
|
1899
|
+
bottom: 0,
|
|
1900
|
+
left: 0,
|
|
1901
|
+
right: 0,
|
|
1902
|
+
height: centerPadding,
|
|
1903
|
+
}}
|
|
1904
|
+
/>
|
|
1730
1905
|
</View>
|
|
1731
1906
|
);
|
|
1732
1907
|
}
|
|
@@ -2,12 +2,16 @@ import type { ClientContext } from './clientContext';
|
|
|
2
2
|
import type { FlowboardData } from '../types/flowboard';
|
|
3
3
|
|
|
4
4
|
const DEFAULT_ENDPOINT = 'https://test-638704832888.europe-west1.run.app';
|
|
5
|
+
const SPECIFIC_ONBOARDING_ENDPOINT =
|
|
6
|
+
'https://onboardsolo-638704832888.europe-west1.run.app';
|
|
5
7
|
|
|
6
8
|
export class ResolverService {
|
|
7
9
|
private endpoint: string;
|
|
10
|
+
private specificOnboardingEndpoint: string;
|
|
8
11
|
|
|
9
12
|
constructor(endpoint?: string) {
|
|
10
13
|
this.endpoint = endpoint ?? DEFAULT_ENDPOINT;
|
|
14
|
+
this.specificOnboardingEndpoint = SPECIFIC_ONBOARDING_ENDPOINT;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
async fetchOnboardingJson(params: {
|
|
@@ -52,7 +56,45 @@ export class ResolverService {
|
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
const body = (await response.json()) as FlowboardData;
|
|
59
|
+
this.applyHeaderMetadata(response, body);
|
|
55
60
|
|
|
61
|
+
return body;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async fetchOnboardingById(params: {
|
|
65
|
+
onboardingId: string;
|
|
66
|
+
locale: string;
|
|
67
|
+
}): Promise<FlowboardData> {
|
|
68
|
+
const payload = {
|
|
69
|
+
onboardingId: params.onboardingId,
|
|
70
|
+
locale: params.locale,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
let response: Response;
|
|
74
|
+
try {
|
|
75
|
+
response = await fetch(this.specificOnboardingEndpoint, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: { 'Content-Type': 'application/json' },
|
|
78
|
+
body: JSON.stringify(payload),
|
|
79
|
+
});
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new Error(`Failed to connect to resolver: ${String(error)}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
const body = await response.text();
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Failed to load onboarding config: ${response.status} ${body}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const body = (await response.json()) as FlowboardData;
|
|
92
|
+
this.applyHeaderMetadata(response, body);
|
|
93
|
+
|
|
94
|
+
return body;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private applyHeaderMetadata(response: Response, body: FlowboardData): void {
|
|
56
98
|
const flowId = response.headers.get('x-flowboard-flow-id');
|
|
57
99
|
if (flowId) body.flow_id = flowId;
|
|
58
100
|
const variantId = response.headers.get('x-flowboard-variant-id');
|
|
@@ -63,7 +105,5 @@ export class ResolverService {
|
|
|
63
105
|
if (bucket) body.bucket = bucket;
|
|
64
106
|
const experimentId = response.headers.get('x-flowboard-experiment-id');
|
|
65
107
|
if (experimentId) body.experiment_id = experimentId;
|
|
66
|
-
|
|
67
|
-
return body;
|
|
68
108
|
}
|
|
69
109
|
}
|
package/src/index.tsx
CHANGED