arlo-react-native 0.1.0
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 +63 -0
- package/package.json +14 -0
- package/src/ArloFlowRenderer.tsx +763 -0
- package/src/ArloPresenterRenderer.tsx +59 -0
- package/src/index.ts +23 -0
- package/src/registry.ts +45 -0
- package/src/storage-cache.ts +29 -0
- package/src/types.ts +41 -0
- package/src/useArloPresenter.ts +11 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Arlo React Native
|
|
2
|
+
|
|
3
|
+
React Native renderer for Arlo flows.
|
|
4
|
+
|
|
5
|
+
## Current scope
|
|
6
|
+
|
|
7
|
+
- Renders the active screen from an `arlo-sdk` flow session
|
|
8
|
+
- Handles default interactions for common component types
|
|
9
|
+
- Lets host apps override per-component rendering
|
|
10
|
+
|
|
11
|
+
## Example
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { useEffect, useState } from "react";
|
|
15
|
+
import { createArloClient, createArloPresenter } from "arlo-sdk";
|
|
16
|
+
import { ArloPresenterRenderer, createArloRegistry, createReactNativeFlowCache } from "./src";
|
|
17
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
18
|
+
|
|
19
|
+
const arlo = createArloClient({
|
|
20
|
+
apiKey: "ob_live_xxx",
|
|
21
|
+
projectId: "proj_123",
|
|
22
|
+
baseUrl: "https://your-arlo-domain.com",
|
|
23
|
+
cache: createReactNativeFlowCache({
|
|
24
|
+
storage: AsyncStorage,
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export function OnboardingScreen() {
|
|
29
|
+
const registry = createArloRegistry();
|
|
30
|
+
registry.registerScreen("paywall_v1", ({ session }) => {
|
|
31
|
+
return null;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const [presenter] = useState(() =>
|
|
35
|
+
createArloPresenter({
|
|
36
|
+
client: arlo,
|
|
37
|
+
})
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
async function load() {
|
|
42
|
+
await presenter.presentFlow("welcome");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
void load();
|
|
46
|
+
}, [presenter]);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<ArloPresenterRenderer
|
|
50
|
+
presenter={presenter}
|
|
51
|
+
registry={registry}
|
|
52
|
+
handlers={{
|
|
53
|
+
onOpenUrl({ url }) {
|
|
54
|
+
console.log("Open URL", url);
|
|
55
|
+
},
|
|
56
|
+
onCompleted({ snapshot }) {
|
|
57
|
+
console.log("Flow completed", snapshot.values);
|
|
58
|
+
},
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "arlo-react-native",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"main": "./src/index.ts",
|
|
5
|
+
"types": "./src/index.ts",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"arlo-sdk": "^0.1.0"
|
|
9
|
+
},
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"react": ">=18",
|
|
12
|
+
"react-native": ">=0.73"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Image,
|
|
4
|
+
Pressable,
|
|
5
|
+
ScrollView,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
Text,
|
|
8
|
+
TextInput,
|
|
9
|
+
View,
|
|
10
|
+
} from "react-native";
|
|
11
|
+
|
|
12
|
+
import { applyFlowSessionEffect } from "arlo-sdk";
|
|
13
|
+
import type {
|
|
14
|
+
FlowComponent,
|
|
15
|
+
FlowSessionSnapshot,
|
|
16
|
+
Screen,
|
|
17
|
+
} from "arlo-sdk";
|
|
18
|
+
|
|
19
|
+
import type {
|
|
20
|
+
ArloComponentRenderContext,
|
|
21
|
+
ArloComponentRendererMap,
|
|
22
|
+
ArloFlowRendererProps,
|
|
23
|
+
} from "./types";
|
|
24
|
+
|
|
25
|
+
const IMPORTED_SCREEN_KEYS = new Set([
|
|
26
|
+
"__arlo_imported_code__",
|
|
27
|
+
"__arlo_imported_figma__",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function isImportedPreviewPayload(
|
|
31
|
+
value: unknown
|
|
32
|
+
): value is { kind: "imported-code" | "imported-figma"; version: 1; previewScreen?: Screen } {
|
|
33
|
+
return Boolean(
|
|
34
|
+
value &&
|
|
35
|
+
typeof value === "object" &&
|
|
36
|
+
!Array.isArray(value) &&
|
|
37
|
+
["imported-code", "imported-figma"].includes(
|
|
38
|
+
String((value as { kind?: unknown }).kind ?? "")
|
|
39
|
+
) &&
|
|
40
|
+
(value as { version?: unknown }).version === 1
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getImportedPreviewScreen(screen: Screen): Screen | null {
|
|
45
|
+
if (!screen.customScreenKey || !IMPORTED_SCREEN_KEYS.has(screen.customScreenKey)) return null;
|
|
46
|
+
return isImportedPreviewPayload(screen.customPayload) && screen.customPayload.previewScreen
|
|
47
|
+
? screen.customPayload.previewScreen
|
|
48
|
+
: null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getScreenContainerStyle(screen: Screen) {
|
|
52
|
+
return {
|
|
53
|
+
backgroundColor: screen.style?.backgroundColor ?? "#0b0b0d",
|
|
54
|
+
paddingTop: screen.style?.paddingTop ?? screen.style?.padding ?? 24,
|
|
55
|
+
paddingBottom: screen.style?.paddingBottom ?? screen.style?.padding ?? 24,
|
|
56
|
+
paddingHorizontal: screen.style?.paddingHorizontal ?? screen.style?.padding ?? 20,
|
|
57
|
+
justifyContent: screen.style?.justifyContent ?? "flex-start",
|
|
58
|
+
alignItems: screen.style?.alignItems ?? "stretch",
|
|
59
|
+
} as const;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getComponentWrapperStyle(component: FlowComponent, isAbsoluteScreen: boolean) {
|
|
63
|
+
const layout = component.layout;
|
|
64
|
+
|
|
65
|
+
if (!layout) {
|
|
66
|
+
return isAbsoluteScreen ? { position: "absolute" as const, zIndex: component.order } : styles.componentBlock;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const baseStyle = {
|
|
70
|
+
display: layout.visible === false ? ("none" as const) : ("flex" as const),
|
|
71
|
+
zIndex: layout.zIndex ?? component.order,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (!isAbsoluteScreen && layout.position !== "absolute") {
|
|
75
|
+
return {
|
|
76
|
+
...styles.componentBlock,
|
|
77
|
+
...baseStyle,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
...baseStyle,
|
|
83
|
+
position: "absolute" as const,
|
|
84
|
+
left: layout.x ?? 0,
|
|
85
|
+
top: layout.y ?? 0,
|
|
86
|
+
width: layout.width,
|
|
87
|
+
height: layout.height,
|
|
88
|
+
transform: layout.rotation ? [{ rotate: `${layout.rotation}deg` }] : undefined,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function coerceStringValue(value: unknown): string {
|
|
93
|
+
return typeof value === "string" ? value : "";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function coerceStringArrayValue(value: unknown): string[] {
|
|
97
|
+
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function coerceNumberValue(value: unknown, fallback: number): number {
|
|
101
|
+
return typeof value === "number" ? value : fallback;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getFieldError(snapshot: FlowSessionSnapshot, fieldKey: string): string | null {
|
|
105
|
+
return snapshot.validationErrorsByField[fieldKey] ?? null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function DefaultTextComponent({ component }: { component: Extract<FlowComponent, { type: "TEXT" }> }) {
|
|
109
|
+
return (
|
|
110
|
+
<Text
|
|
111
|
+
style={{
|
|
112
|
+
color: component.props.color ?? "#ffffff",
|
|
113
|
+
fontSize: component.props.fontSize ?? 16,
|
|
114
|
+
fontWeight: component.props.fontWeight ?? "normal",
|
|
115
|
+
textAlign: component.props.textAlign ?? "left",
|
|
116
|
+
lineHeight:
|
|
117
|
+
component.props.lineHeight && component.props.fontSize
|
|
118
|
+
? component.props.lineHeight * component.props.fontSize
|
|
119
|
+
: undefined,
|
|
120
|
+
opacity: component.props.opacity ?? 1,
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
{component.props.content}
|
|
124
|
+
</Text>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function DefaultImageComponent({ component }: { component: Extract<FlowComponent, { type: "IMAGE" }> }) {
|
|
129
|
+
return (
|
|
130
|
+
<Image
|
|
131
|
+
source={{ uri: component.props.src }}
|
|
132
|
+
accessibilityLabel={component.props.alt}
|
|
133
|
+
resizeMode={component.props.resizeMode ?? "cover"}
|
|
134
|
+
style={{
|
|
135
|
+
width: component.props.width ?? "100%",
|
|
136
|
+
height: component.props.height ?? 220,
|
|
137
|
+
borderRadius: component.props.borderRadius ?? 0,
|
|
138
|
+
}}
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function DefaultButtonComponent({
|
|
144
|
+
component,
|
|
145
|
+
onPress,
|
|
146
|
+
}: {
|
|
147
|
+
component: Extract<FlowComponent, { type: "BUTTON" }>;
|
|
148
|
+
onPress: () => Promise<void>;
|
|
149
|
+
}) {
|
|
150
|
+
return (
|
|
151
|
+
<Pressable
|
|
152
|
+
onPress={() => {
|
|
153
|
+
void onPress();
|
|
154
|
+
}}
|
|
155
|
+
style={[
|
|
156
|
+
styles.button,
|
|
157
|
+
{
|
|
158
|
+
backgroundColor: component.props.style?.backgroundColor ?? "#ffffff",
|
|
159
|
+
borderRadius: component.props.style?.borderRadius ?? 14,
|
|
160
|
+
borderColor: component.props.style?.borderColor ?? "transparent",
|
|
161
|
+
borderWidth: component.props.style?.borderWidth ?? 0,
|
|
162
|
+
},
|
|
163
|
+
]}
|
|
164
|
+
>
|
|
165
|
+
<Text
|
|
166
|
+
style={[
|
|
167
|
+
styles.buttonText,
|
|
168
|
+
{
|
|
169
|
+
color: component.props.style?.textColor ?? "#111111",
|
|
170
|
+
},
|
|
171
|
+
]}
|
|
172
|
+
>
|
|
173
|
+
{component.props.label}
|
|
174
|
+
</Text>
|
|
175
|
+
</Pressable>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function DefaultTextInputComponent({
|
|
180
|
+
component,
|
|
181
|
+
context,
|
|
182
|
+
}: {
|
|
183
|
+
component: Extract<FlowComponent, { type: "TEXT_INPUT" }>;
|
|
184
|
+
context: ArloComponentRenderContext;
|
|
185
|
+
}) {
|
|
186
|
+
const value = coerceStringValue(context.snapshot.values[component.props.fieldKey]);
|
|
187
|
+
const error = getFieldError(context.snapshot, component.props.fieldKey);
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<View style={styles.fieldGroup}>
|
|
191
|
+
{component.props.label ? <Text style={styles.fieldLabel}>{component.props.label}</Text> : null}
|
|
192
|
+
<TextInput
|
|
193
|
+
value={value}
|
|
194
|
+
onChangeText={(nextValue) => context.onValueChange(component.props.fieldKey, nextValue)}
|
|
195
|
+
placeholder={component.props.placeholder}
|
|
196
|
+
placeholderTextColor="#7a7a85"
|
|
197
|
+
keyboardType={
|
|
198
|
+
component.props.keyboardType === "email"
|
|
199
|
+
? "email-address"
|
|
200
|
+
: component.props.keyboardType === "numeric"
|
|
201
|
+
? "numeric"
|
|
202
|
+
: component.props.keyboardType === "phone"
|
|
203
|
+
? "phone-pad"
|
|
204
|
+
: "default"
|
|
205
|
+
}
|
|
206
|
+
maxLength={component.props.maxLength}
|
|
207
|
+
style={[styles.input, error ? styles.inputError : undefined]}
|
|
208
|
+
/>
|
|
209
|
+
{error ? <Text style={styles.fieldError}>{error}</Text> : null}
|
|
210
|
+
</View>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function OptionPill({
|
|
215
|
+
label,
|
|
216
|
+
selected,
|
|
217
|
+
onPress,
|
|
218
|
+
}: {
|
|
219
|
+
label: string;
|
|
220
|
+
selected: boolean;
|
|
221
|
+
onPress: () => void;
|
|
222
|
+
}) {
|
|
223
|
+
return (
|
|
224
|
+
<Pressable
|
|
225
|
+
onPress={onPress}
|
|
226
|
+
style={[styles.optionPill, selected ? styles.optionPillSelected : undefined]}
|
|
227
|
+
>
|
|
228
|
+
<Text style={[styles.optionPillText, selected ? styles.optionPillTextSelected : undefined]}>
|
|
229
|
+
{label}
|
|
230
|
+
</Text>
|
|
231
|
+
</Pressable>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function DefaultSingleSelectComponent({
|
|
236
|
+
component,
|
|
237
|
+
context,
|
|
238
|
+
}: {
|
|
239
|
+
component: Extract<FlowComponent, { type: "SINGLE_SELECT" }>;
|
|
240
|
+
context: ArloComponentRenderContext;
|
|
241
|
+
}) {
|
|
242
|
+
const value = coerceStringValue(context.snapshot.values[component.props.fieldKey]);
|
|
243
|
+
const error = getFieldError(context.snapshot, component.props.fieldKey);
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<View style={styles.fieldGroup}>
|
|
247
|
+
{component.props.label ? <Text style={styles.fieldLabel}>{component.props.label}</Text> : null}
|
|
248
|
+
<View style={styles.optionGroup}>
|
|
249
|
+
{component.props.options.map((option) => (
|
|
250
|
+
<OptionPill
|
|
251
|
+
key={option.id}
|
|
252
|
+
label={option.label}
|
|
253
|
+
selected={value === option.id}
|
|
254
|
+
onPress={() => context.onValueChange(component.props.fieldKey, option.id)}
|
|
255
|
+
/>
|
|
256
|
+
))}
|
|
257
|
+
</View>
|
|
258
|
+
{error ? <Text style={styles.fieldError}>{error}</Text> : null}
|
|
259
|
+
</View>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function DefaultMultiSelectComponent({
|
|
264
|
+
component,
|
|
265
|
+
context,
|
|
266
|
+
}: {
|
|
267
|
+
component: Extract<FlowComponent, { type: "MULTI_SELECT" }>;
|
|
268
|
+
context: ArloComponentRenderContext;
|
|
269
|
+
}) {
|
|
270
|
+
const values = coerceStringArrayValue(context.snapshot.values[component.props.fieldKey]);
|
|
271
|
+
const error = getFieldError(context.snapshot, component.props.fieldKey);
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<View style={styles.fieldGroup}>
|
|
275
|
+
{component.props.label ? <Text style={styles.fieldLabel}>{component.props.label}</Text> : null}
|
|
276
|
+
<View style={styles.optionGroup}>
|
|
277
|
+
{component.props.options.map((option) => {
|
|
278
|
+
const selected = values.includes(option.id);
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<OptionPill
|
|
282
|
+
key={option.id}
|
|
283
|
+
label={option.label}
|
|
284
|
+
selected={selected}
|
|
285
|
+
onPress={() => {
|
|
286
|
+
const nextValues = selected
|
|
287
|
+
? values.filter((value) => value !== option.id)
|
|
288
|
+
: [...values, option.id];
|
|
289
|
+
|
|
290
|
+
context.onValueChange(component.props.fieldKey, nextValues);
|
|
291
|
+
}}
|
|
292
|
+
/>
|
|
293
|
+
);
|
|
294
|
+
})}
|
|
295
|
+
</View>
|
|
296
|
+
{error ? <Text style={styles.fieldError}>{error}</Text> : null}
|
|
297
|
+
</View>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function DefaultSliderComponent({
|
|
302
|
+
component,
|
|
303
|
+
context,
|
|
304
|
+
}: {
|
|
305
|
+
component: Extract<FlowComponent, { type: "SLIDER" }>;
|
|
306
|
+
context: ArloComponentRenderContext;
|
|
307
|
+
}) {
|
|
308
|
+
const currentValue = coerceNumberValue(
|
|
309
|
+
context.snapshot.values[component.props.fieldKey],
|
|
310
|
+
component.props.defaultValue ?? component.props.min
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const step = component.props.step ?? 1;
|
|
314
|
+
const nextValue = Math.min(component.props.max, currentValue + step);
|
|
315
|
+
const previousValue = Math.max(component.props.min, currentValue - step);
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<View style={styles.fieldGroup}>
|
|
319
|
+
{component.props.label ? <Text style={styles.fieldLabel}>{component.props.label}</Text> : null}
|
|
320
|
+
<View style={styles.sliderCard}>
|
|
321
|
+
<Text style={styles.sliderValue}>{String(currentValue)}</Text>
|
|
322
|
+
<View style={styles.sliderActions}>
|
|
323
|
+
<OptionPill
|
|
324
|
+
label={component.props.minLabel ?? "-"}
|
|
325
|
+
selected={false}
|
|
326
|
+
onPress={() => context.onValueChange(component.props.fieldKey, previousValue)}
|
|
327
|
+
/>
|
|
328
|
+
<OptionPill
|
|
329
|
+
label={component.props.maxLabel ?? "+"}
|
|
330
|
+
selected={false}
|
|
331
|
+
onPress={() => context.onValueChange(component.props.fieldKey, nextValue)}
|
|
332
|
+
/>
|
|
333
|
+
</View>
|
|
334
|
+
</View>
|
|
335
|
+
</View>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function DefaultProgressBarComponent({
|
|
340
|
+
snapshot,
|
|
341
|
+
component,
|
|
342
|
+
}: {
|
|
343
|
+
snapshot: FlowSessionSnapshot;
|
|
344
|
+
component: Extract<FlowComponent, { type: "PROGRESS_BAR" }>;
|
|
345
|
+
}) {
|
|
346
|
+
const progress =
|
|
347
|
+
snapshot.totalScreens > 1
|
|
348
|
+
? ((snapshot.currentScreenIndex + 1) / snapshot.totalScreens) * 100
|
|
349
|
+
: 100;
|
|
350
|
+
|
|
351
|
+
return (
|
|
352
|
+
<View
|
|
353
|
+
style={[
|
|
354
|
+
styles.progressTrack,
|
|
355
|
+
{
|
|
356
|
+
backgroundColor: component.props.backgroundColor ?? "#26262b",
|
|
357
|
+
height: component.props.height ?? 6,
|
|
358
|
+
},
|
|
359
|
+
]}
|
|
360
|
+
>
|
|
361
|
+
<View
|
|
362
|
+
style={{
|
|
363
|
+
width: `${progress}%`,
|
|
364
|
+
backgroundColor: component.props.color ?? "#ffffff",
|
|
365
|
+
height: "100%",
|
|
366
|
+
borderRadius: 999,
|
|
367
|
+
}}
|
|
368
|
+
/>
|
|
369
|
+
</View>
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function DefaultPageIndicatorComponent({
|
|
374
|
+
snapshot,
|
|
375
|
+
component,
|
|
376
|
+
}: {
|
|
377
|
+
snapshot: FlowSessionSnapshot;
|
|
378
|
+
component: Extract<FlowComponent, { type: "PAGE_INDICATOR" }>;
|
|
379
|
+
}) {
|
|
380
|
+
const size = component.props.size ?? 8;
|
|
381
|
+
|
|
382
|
+
return (
|
|
383
|
+
<View style={styles.pageIndicatorRow}>
|
|
384
|
+
{Array.from({ length: snapshot.totalScreens }).map((_, index) => (
|
|
385
|
+
<View
|
|
386
|
+
key={index}
|
|
387
|
+
style={{
|
|
388
|
+
width: size,
|
|
389
|
+
height: size,
|
|
390
|
+
borderRadius: size / 2,
|
|
391
|
+
backgroundColor:
|
|
392
|
+
index === snapshot.currentScreenIndex
|
|
393
|
+
? component.props.activeColor ?? "#ffffff"
|
|
394
|
+
: component.props.inactiveColor ?? "#4b4b55",
|
|
395
|
+
}}
|
|
396
|
+
/>
|
|
397
|
+
))}
|
|
398
|
+
</View>
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function renderDefaultComponent(
|
|
403
|
+
component: FlowComponent,
|
|
404
|
+
context: ArloComponentRenderContext,
|
|
405
|
+
registry?: ArloFlowRendererProps["registry"]
|
|
406
|
+
) {
|
|
407
|
+
switch (component.type) {
|
|
408
|
+
case "TEXT":
|
|
409
|
+
return <DefaultTextComponent component={component} />;
|
|
410
|
+
case "IMAGE":
|
|
411
|
+
return <DefaultImageComponent component={component} />;
|
|
412
|
+
case "BUTTON":
|
|
413
|
+
return <DefaultButtonComponent component={component} onPress={() => context.onPressButton(component.id)} />;
|
|
414
|
+
case "TEXT_INPUT":
|
|
415
|
+
return <DefaultTextInputComponent component={component} context={context} />;
|
|
416
|
+
case "SINGLE_SELECT":
|
|
417
|
+
return <DefaultSingleSelectComponent component={component} context={context} />;
|
|
418
|
+
case "MULTI_SELECT":
|
|
419
|
+
return <DefaultMultiSelectComponent component={component} context={context} />;
|
|
420
|
+
case "SLIDER":
|
|
421
|
+
return <DefaultSliderComponent component={component} context={context} />;
|
|
422
|
+
case "PROGRESS_BAR":
|
|
423
|
+
return <DefaultProgressBarComponent component={component} snapshot={context.snapshot} />;
|
|
424
|
+
case "PAGE_INDICATOR":
|
|
425
|
+
return <DefaultPageIndicatorComponent component={component} snapshot={context.snapshot} />;
|
|
426
|
+
case "CUSTOM_COMPONENT": {
|
|
427
|
+
const registered = registry?.getComponent(component.props.registryKey);
|
|
428
|
+
return registered
|
|
429
|
+
? registered({
|
|
430
|
+
session: context.session,
|
|
431
|
+
snapshot: context.snapshot,
|
|
432
|
+
screen: context.snapshot.currentScreen!,
|
|
433
|
+
component,
|
|
434
|
+
})
|
|
435
|
+
: null;
|
|
436
|
+
}
|
|
437
|
+
default:
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function ArloFlowRenderer({
|
|
443
|
+
session,
|
|
444
|
+
handlers,
|
|
445
|
+
componentRenderers,
|
|
446
|
+
registry,
|
|
447
|
+
autoStart = true,
|
|
448
|
+
emptyState = null,
|
|
449
|
+
unsupportedComponent,
|
|
450
|
+
unsupportedScreen,
|
|
451
|
+
onSnapshotChange,
|
|
452
|
+
}: ArloFlowRendererProps) {
|
|
453
|
+
const [snapshot, setSnapshot] = useState(() => session.getSnapshot());
|
|
454
|
+
|
|
455
|
+
useEffect(() => {
|
|
456
|
+
const nextSnapshot = session.getSnapshot();
|
|
457
|
+
setSnapshot(nextSnapshot);
|
|
458
|
+
onSnapshotChange?.(nextSnapshot);
|
|
459
|
+
|
|
460
|
+
if (autoStart && session.getSnapshot().status === "idle") {
|
|
461
|
+
const effect = session.start();
|
|
462
|
+
const startedSnapshot = session.getSnapshot();
|
|
463
|
+
setSnapshot(startedSnapshot);
|
|
464
|
+
onSnapshotChange?.(startedSnapshot);
|
|
465
|
+
void applyFlowSessionEffect(session, effect, handlers);
|
|
466
|
+
}
|
|
467
|
+
}, [autoStart, handlers, onSnapshotChange, session]);
|
|
468
|
+
|
|
469
|
+
const sortedComponents = useMemo(
|
|
470
|
+
() =>
|
|
471
|
+
[...(snapshot.currentScreen?.components ?? [])].sort((a, b) => a.order - b.order),
|
|
472
|
+
[snapshot.currentScreen]
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
const context: ArloComponentRenderContext = {
|
|
476
|
+
session,
|
|
477
|
+
snapshot,
|
|
478
|
+
handlers,
|
|
479
|
+
onValueChange: (fieldKey, value) => {
|
|
480
|
+
const nextSnapshot = session.setValue(fieldKey, value);
|
|
481
|
+
setSnapshot(nextSnapshot);
|
|
482
|
+
onSnapshotChange?.(nextSnapshot);
|
|
483
|
+
},
|
|
484
|
+
onPressButton: async (componentId) => {
|
|
485
|
+
const effect = session.pressButton(componentId);
|
|
486
|
+
const immediateSnapshot = session.getSnapshot();
|
|
487
|
+
setSnapshot(immediateSnapshot);
|
|
488
|
+
onSnapshotChange?.(immediateSnapshot);
|
|
489
|
+
await applyFlowSessionEffect(session, effect, handlers);
|
|
490
|
+
const finalSnapshot = session.getSnapshot();
|
|
491
|
+
setSnapshot(finalSnapshot);
|
|
492
|
+
onSnapshotChange?.(finalSnapshot);
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
if (!snapshot.currentScreen) {
|
|
497
|
+
return <>{emptyState}</>;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (snapshot.currentScreen.customScreenKey) {
|
|
501
|
+
const registeredScreen = registry?.getScreen(snapshot.currentScreen.customScreenKey);
|
|
502
|
+
const importedPreviewScreen = getImportedPreviewScreen(snapshot.currentScreen);
|
|
503
|
+
|
|
504
|
+
if (registeredScreen) {
|
|
505
|
+
return (
|
|
506
|
+
<>
|
|
507
|
+
{registeredScreen({
|
|
508
|
+
session,
|
|
509
|
+
snapshot,
|
|
510
|
+
screen: snapshot.currentScreen,
|
|
511
|
+
})}
|
|
512
|
+
</>
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (importedPreviewScreen) {
|
|
517
|
+
const previewComponents = [...(importedPreviewScreen.components ?? [])].sort(
|
|
518
|
+
(a, b) => a.order - b.order
|
|
519
|
+
);
|
|
520
|
+
const previewContext: ArloComponentRenderContext = {
|
|
521
|
+
...context,
|
|
522
|
+
onValueChange: () => undefined,
|
|
523
|
+
onPressButton: async () => undefined,
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
return (
|
|
527
|
+
<ScrollView
|
|
528
|
+
contentContainerStyle={[
|
|
529
|
+
styles.container,
|
|
530
|
+
getScreenContainerStyle(importedPreviewScreen),
|
|
531
|
+
]}
|
|
532
|
+
>
|
|
533
|
+
{previewComponents.map((component) => {
|
|
534
|
+
const customRenderer = componentRenderers?.[component.type] as
|
|
535
|
+
| ArloComponentRendererMap[typeof component.type]
|
|
536
|
+
| undefined;
|
|
537
|
+
|
|
538
|
+
const content = customRenderer
|
|
539
|
+
? customRenderer(component as never, previewContext as never)
|
|
540
|
+
: renderDefaultComponent(component, previewContext, registry);
|
|
541
|
+
|
|
542
|
+
if (content === null) {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return <View key={component.id}>{content}</View>;
|
|
547
|
+
})}
|
|
548
|
+
</ScrollView>
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return (
|
|
553
|
+
<>
|
|
554
|
+
{unsupportedScreen ? (
|
|
555
|
+
unsupportedScreen(snapshot.currentScreen)
|
|
556
|
+
) : (
|
|
557
|
+
<View style={styles.unsupported}>
|
|
558
|
+
<Text style={styles.unsupportedText}>
|
|
559
|
+
Unsupported screen: {snapshot.currentScreen.customScreenKey}
|
|
560
|
+
</Text>
|
|
561
|
+
</View>
|
|
562
|
+
)}
|
|
563
|
+
</>
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (snapshot.currentScreen.layoutMode === "absolute") {
|
|
568
|
+
return (
|
|
569
|
+
<View
|
|
570
|
+
style={[
|
|
571
|
+
styles.absoluteContainer,
|
|
572
|
+
getScreenContainerStyle(snapshot.currentScreen),
|
|
573
|
+
]}
|
|
574
|
+
>
|
|
575
|
+
{sortedComponents.map((component) => {
|
|
576
|
+
const customRenderer = componentRenderers?.[component.type] as
|
|
577
|
+
| ArloComponentRendererMap[typeof component.type]
|
|
578
|
+
| undefined;
|
|
579
|
+
|
|
580
|
+
const content = customRenderer
|
|
581
|
+
? customRenderer(component as never, context as never)
|
|
582
|
+
: renderDefaultComponent(component, context, registry);
|
|
583
|
+
|
|
584
|
+
if (content === null) {
|
|
585
|
+
return (
|
|
586
|
+
<View key={component.id} style={styles.unsupported}>
|
|
587
|
+
{unsupportedComponent ? (
|
|
588
|
+
unsupportedComponent(component)
|
|
589
|
+
) : (
|
|
590
|
+
<Text style={styles.unsupportedText}>
|
|
591
|
+
Unsupported component: {component.type}
|
|
592
|
+
</Text>
|
|
593
|
+
)}
|
|
594
|
+
</View>
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return (
|
|
599
|
+
<View
|
|
600
|
+
key={component.id}
|
|
601
|
+
style={getComponentWrapperStyle(component, true)}
|
|
602
|
+
>
|
|
603
|
+
{content}
|
|
604
|
+
</View>
|
|
605
|
+
);
|
|
606
|
+
})}
|
|
607
|
+
</View>
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return (
|
|
612
|
+
<ScrollView
|
|
613
|
+
contentContainerStyle={[
|
|
614
|
+
styles.container,
|
|
615
|
+
getScreenContainerStyle(snapshot.currentScreen),
|
|
616
|
+
]}
|
|
617
|
+
>
|
|
618
|
+
{sortedComponents.map((component) => {
|
|
619
|
+
const customRenderer = componentRenderers?.[component.type] as
|
|
620
|
+
| ArloComponentRendererMap[typeof component.type]
|
|
621
|
+
| undefined;
|
|
622
|
+
|
|
623
|
+
const content = customRenderer
|
|
624
|
+
? customRenderer(component as never, context as never)
|
|
625
|
+
: renderDefaultComponent(component, context, registry);
|
|
626
|
+
|
|
627
|
+
if (content === null) {
|
|
628
|
+
return (
|
|
629
|
+
<View key={component.id} style={styles.unsupported}>
|
|
630
|
+
{unsupportedComponent ? (
|
|
631
|
+
unsupportedComponent(component)
|
|
632
|
+
) : (
|
|
633
|
+
<Text style={styles.unsupportedText}>
|
|
634
|
+
Unsupported component: {component.type}
|
|
635
|
+
</Text>
|
|
636
|
+
)}
|
|
637
|
+
</View>
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return (
|
|
642
|
+
<View key={component.id} style={getComponentWrapperStyle(component, false)}>
|
|
643
|
+
{content}
|
|
644
|
+
</View>
|
|
645
|
+
);
|
|
646
|
+
})}
|
|
647
|
+
</ScrollView>
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const styles = StyleSheet.create({
|
|
652
|
+
container: {
|
|
653
|
+
flexGrow: 1,
|
|
654
|
+
gap: 16,
|
|
655
|
+
},
|
|
656
|
+
componentBlock: {
|
|
657
|
+
width: "100%",
|
|
658
|
+
},
|
|
659
|
+
absoluteContainer: {
|
|
660
|
+
flex: 1,
|
|
661
|
+
position: "relative",
|
|
662
|
+
overflow: "hidden",
|
|
663
|
+
},
|
|
664
|
+
fieldGroup: {
|
|
665
|
+
gap: 8,
|
|
666
|
+
},
|
|
667
|
+
fieldLabel: {
|
|
668
|
+
color: "#f3f3f5",
|
|
669
|
+
fontSize: 14,
|
|
670
|
+
fontWeight: "600",
|
|
671
|
+
},
|
|
672
|
+
input: {
|
|
673
|
+
borderWidth: 1,
|
|
674
|
+
borderColor: "#2c2c34",
|
|
675
|
+
borderRadius: 14,
|
|
676
|
+
paddingHorizontal: 14,
|
|
677
|
+
paddingVertical: 12,
|
|
678
|
+
color: "#ffffff",
|
|
679
|
+
backgroundColor: "#141419",
|
|
680
|
+
},
|
|
681
|
+
inputError: {
|
|
682
|
+
borderColor: "#f36b8d",
|
|
683
|
+
},
|
|
684
|
+
fieldError: {
|
|
685
|
+
color: "#f59cb3",
|
|
686
|
+
fontSize: 12,
|
|
687
|
+
fontWeight: "500",
|
|
688
|
+
},
|
|
689
|
+
button: {
|
|
690
|
+
minHeight: 52,
|
|
691
|
+
alignItems: "center",
|
|
692
|
+
justifyContent: "center",
|
|
693
|
+
paddingHorizontal: 16,
|
|
694
|
+
paddingVertical: 12,
|
|
695
|
+
},
|
|
696
|
+
buttonText: {
|
|
697
|
+
fontSize: 16,
|
|
698
|
+
fontWeight: "700",
|
|
699
|
+
},
|
|
700
|
+
optionGroup: {
|
|
701
|
+
flexDirection: "row",
|
|
702
|
+
flexWrap: "wrap",
|
|
703
|
+
gap: 10,
|
|
704
|
+
},
|
|
705
|
+
optionPill: {
|
|
706
|
+
paddingHorizontal: 14,
|
|
707
|
+
paddingVertical: 10,
|
|
708
|
+
borderRadius: 999,
|
|
709
|
+
borderWidth: 1,
|
|
710
|
+
borderColor: "#30303a",
|
|
711
|
+
backgroundColor: "#15151b",
|
|
712
|
+
},
|
|
713
|
+
optionPillSelected: {
|
|
714
|
+
backgroundColor: "#ffffff",
|
|
715
|
+
borderColor: "#ffffff",
|
|
716
|
+
},
|
|
717
|
+
optionPillText: {
|
|
718
|
+
color: "#f1f1f3",
|
|
719
|
+
fontSize: 14,
|
|
720
|
+
fontWeight: "600",
|
|
721
|
+
},
|
|
722
|
+
optionPillTextSelected: {
|
|
723
|
+
color: "#111111",
|
|
724
|
+
},
|
|
725
|
+
sliderCard: {
|
|
726
|
+
borderRadius: 18,
|
|
727
|
+
backgroundColor: "#15151b",
|
|
728
|
+
borderWidth: 1,
|
|
729
|
+
borderColor: "#2b2b34",
|
|
730
|
+
padding: 14,
|
|
731
|
+
gap: 12,
|
|
732
|
+
},
|
|
733
|
+
sliderValue: {
|
|
734
|
+
color: "#ffffff",
|
|
735
|
+
fontSize: 28,
|
|
736
|
+
fontWeight: "700",
|
|
737
|
+
},
|
|
738
|
+
sliderActions: {
|
|
739
|
+
flexDirection: "row",
|
|
740
|
+
gap: 10,
|
|
741
|
+
},
|
|
742
|
+
progressTrack: {
|
|
743
|
+
width: "100%",
|
|
744
|
+
borderRadius: 999,
|
|
745
|
+
overflow: "hidden",
|
|
746
|
+
},
|
|
747
|
+
pageIndicatorRow: {
|
|
748
|
+
flexDirection: "row",
|
|
749
|
+
gap: 8,
|
|
750
|
+
alignItems: "center",
|
|
751
|
+
},
|
|
752
|
+
unsupported: {
|
|
753
|
+
padding: 12,
|
|
754
|
+
borderRadius: 12,
|
|
755
|
+
borderWidth: 1,
|
|
756
|
+
borderColor: "#3a2430",
|
|
757
|
+
backgroundColor: "#21151a",
|
|
758
|
+
},
|
|
759
|
+
unsupportedText: {
|
|
760
|
+
color: "#f5b4c6",
|
|
761
|
+
fontSize: 13,
|
|
762
|
+
},
|
|
763
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Text, View } from "react-native";
|
|
3
|
+
|
|
4
|
+
import type { ArloPresenter } from "arlo-sdk";
|
|
5
|
+
|
|
6
|
+
import { ArloFlowRenderer } from "./ArloFlowRenderer";
|
|
7
|
+
import type { ArloFlowRendererProps } from "./types";
|
|
8
|
+
import { useArloPresenter } from "./useArloPresenter";
|
|
9
|
+
|
|
10
|
+
export interface ArloPresenterRendererProps
|
|
11
|
+
extends Omit<ArloFlowRendererProps, "session"> {
|
|
12
|
+
presenter: ArloPresenter;
|
|
13
|
+
loadingState?: ReactNode;
|
|
14
|
+
errorState?: (message: string) => ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ArloPresenterRenderer({
|
|
18
|
+
presenter,
|
|
19
|
+
loadingState = (
|
|
20
|
+
<View>
|
|
21
|
+
<Text>Loading...</Text>
|
|
22
|
+
</View>
|
|
23
|
+
),
|
|
24
|
+
errorState,
|
|
25
|
+
...rendererProps
|
|
26
|
+
}: ArloPresenterRendererProps) {
|
|
27
|
+
const state = useArloPresenter(presenter);
|
|
28
|
+
|
|
29
|
+
if (state.status === "loading") {
|
|
30
|
+
return <>{loadingState}</>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (state.status === "error") {
|
|
34
|
+
if (errorState) {
|
|
35
|
+
return <>{errorState(state.error?.message ?? "Unknown error")}</>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<View>
|
|
40
|
+
<Text>{state.error?.message ?? "Failed to load flow"}</Text>
|
|
41
|
+
</View>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!state.session) {
|
|
46
|
+
return <>{rendererProps.emptyState ?? null}</>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<ArloFlowRenderer
|
|
51
|
+
session={state.session}
|
|
52
|
+
{...rendererProps}
|
|
53
|
+
onSnapshotChange={(snapshot) => {
|
|
54
|
+
rendererProps.onSnapshotChange?.(snapshot);
|
|
55
|
+
presenter.syncSession();
|
|
56
|
+
}}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export { ArloFlowRenderer } from "./ArloFlowRenderer";
|
|
2
|
+
export { ArloPresenterRenderer } from "./ArloPresenterRenderer";
|
|
3
|
+
export { createArloRegistry } from "./registry";
|
|
4
|
+
export { createReactNativeFlowCache } from "./storage-cache";
|
|
5
|
+
export { useArloPresenter } from "./useArloPresenter";
|
|
6
|
+
export type {
|
|
7
|
+
ArloComponentRenderContext,
|
|
8
|
+
ArloComponentRenderer,
|
|
9
|
+
ArloComponentRendererMap,
|
|
10
|
+
ArloCustomScreenRenderer,
|
|
11
|
+
ArloFlowRendererProps,
|
|
12
|
+
} from "./types";
|
|
13
|
+
export type {
|
|
14
|
+
ArloRegistry,
|
|
15
|
+
ArloRegisteredComponent,
|
|
16
|
+
ArloRegisteredScreen,
|
|
17
|
+
ArloRegistryComponentContext,
|
|
18
|
+
ArloRegistryScreenContext,
|
|
19
|
+
} from "./registry";
|
|
20
|
+
export type {
|
|
21
|
+
AsyncStorageLike,
|
|
22
|
+
CreateReactNativeFlowCacheOptions,
|
|
23
|
+
} from "./storage-cache";
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import type { FlowComponent, FlowSession, FlowSessionSnapshot, Screen } from "arlo-sdk";
|
|
4
|
+
|
|
5
|
+
export interface ArloRegistryScreenContext {
|
|
6
|
+
session: FlowSession;
|
|
7
|
+
snapshot: FlowSessionSnapshot;
|
|
8
|
+
screen: Screen;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ArloRegistryComponentContext extends ArloRegistryScreenContext {
|
|
12
|
+
component: Extract<FlowComponent, { type: "CUSTOM_COMPONENT" }>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type ArloRegisteredScreen = (context: ArloRegistryScreenContext) => ReactNode;
|
|
16
|
+
export type ArloRegisteredComponent = (
|
|
17
|
+
context: ArloRegistryComponentContext
|
|
18
|
+
) => ReactNode;
|
|
19
|
+
|
|
20
|
+
export interface ArloRegistry {
|
|
21
|
+
registerScreen(key: string, renderer: ArloRegisteredScreen): void;
|
|
22
|
+
registerComponent(key: string, renderer: ArloRegisteredComponent): void;
|
|
23
|
+
getScreen(key: string): ArloRegisteredScreen | null;
|
|
24
|
+
getComponent(key: string): ArloRegisteredComponent | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createArloRegistry(): ArloRegistry {
|
|
28
|
+
const screens = new Map<string, ArloRegisteredScreen>();
|
|
29
|
+
const components = new Map<string, ArloRegisteredComponent>();
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
registerScreen(key: string, renderer: ArloRegisteredScreen): void {
|
|
33
|
+
screens.set(key, renderer);
|
|
34
|
+
},
|
|
35
|
+
registerComponent(key: string, renderer: ArloRegisteredComponent): void {
|
|
36
|
+
components.set(key, renderer);
|
|
37
|
+
},
|
|
38
|
+
getScreen(key: string): ArloRegisteredScreen | null {
|
|
39
|
+
return screens.get(key) ?? null;
|
|
40
|
+
},
|
|
41
|
+
getComponent(key: string): ArloRegisteredComponent | null {
|
|
42
|
+
return components.get(key) ?? null;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createPersistentFlowCache, type ArloCacheStorage } from "arlo-sdk";
|
|
2
|
+
|
|
3
|
+
export interface AsyncStorageLike {
|
|
4
|
+
getItem(key: string): Promise<string | null>;
|
|
5
|
+
setItem(key: string, value: string): Promise<void>;
|
|
6
|
+
removeItem(key: string): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface CreateReactNativeFlowCacheOptions {
|
|
10
|
+
storage: AsyncStorageLike;
|
|
11
|
+
namespace?: string;
|
|
12
|
+
maxAgeMs?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createReactNativeFlowCache(
|
|
16
|
+
options: CreateReactNativeFlowCacheOptions
|
|
17
|
+
) {
|
|
18
|
+
const storage: ArloCacheStorage = {
|
|
19
|
+
getItem: options.storage.getItem.bind(options.storage),
|
|
20
|
+
setItem: options.storage.setItem.bind(options.storage),
|
|
21
|
+
removeItem: options.storage.removeItem.bind(options.storage),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return createPersistentFlowCache({
|
|
25
|
+
storage,
|
|
26
|
+
namespace: options.namespace,
|
|
27
|
+
maxAgeMs: options.maxAgeMs,
|
|
28
|
+
});
|
|
29
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
FlowBridgeHandlers,
|
|
5
|
+
FlowComponent,
|
|
6
|
+
FlowSession,
|
|
7
|
+
FlowSessionSnapshot,
|
|
8
|
+
Screen,
|
|
9
|
+
} from "arlo-sdk";
|
|
10
|
+
import type { ArloRegistry } from "./registry";
|
|
11
|
+
|
|
12
|
+
export interface ArloComponentRenderContext {
|
|
13
|
+
session: FlowSession;
|
|
14
|
+
snapshot: FlowSessionSnapshot;
|
|
15
|
+
handlers?: FlowBridgeHandlers;
|
|
16
|
+
onValueChange: (fieldKey: string, value: string | string[] | number | boolean | null) => void;
|
|
17
|
+
onPressButton: (componentId: string) => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ArloComponentRenderer<T extends FlowComponent = FlowComponent> = (
|
|
21
|
+
component: T,
|
|
22
|
+
context: ArloComponentRenderContext
|
|
23
|
+
) => ReactNode;
|
|
24
|
+
|
|
25
|
+
export type ArloComponentRendererMap = Partial<{
|
|
26
|
+
[K in FlowComponent["type"]]: ArloComponentRenderer<Extract<FlowComponent, { type: K }>>;
|
|
27
|
+
}>;
|
|
28
|
+
|
|
29
|
+
export type ArloCustomScreenRenderer = (screen: Screen, context: ArloComponentRenderContext) => ReactNode;
|
|
30
|
+
|
|
31
|
+
export interface ArloFlowRendererProps {
|
|
32
|
+
session: FlowSession;
|
|
33
|
+
handlers?: FlowBridgeHandlers;
|
|
34
|
+
componentRenderers?: ArloComponentRendererMap;
|
|
35
|
+
registry?: ArloRegistry;
|
|
36
|
+
autoStart?: boolean;
|
|
37
|
+
emptyState?: ReactNode;
|
|
38
|
+
unsupportedComponent?: (component: FlowComponent) => ReactNode;
|
|
39
|
+
unsupportedScreen?: (screen: Screen) => ReactNode;
|
|
40
|
+
onSnapshotChange?: (snapshot: FlowSessionSnapshot) => void;
|
|
41
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import type { ArloPresentationState, ArloPresenter } from "arlo-sdk";
|
|
4
|
+
|
|
5
|
+
export function useArloPresenter(presenter: ArloPresenter): ArloPresentationState {
|
|
6
|
+
const [state, setState] = useState<ArloPresentationState>(() => presenter.getState());
|
|
7
|
+
|
|
8
|
+
useEffect(() => presenter.subscribe(setState), [presenter]);
|
|
9
|
+
|
|
10
|
+
return state;
|
|
11
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noEmit": true,
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"module": "esnext",
|
|
7
|
+
"target": "ES2020",
|
|
8
|
+
"lib": ["ES2020", "DOM"],
|
|
9
|
+
"jsx": "react-jsx",
|
|
10
|
+
"baseUrl": ".",
|
|
11
|
+
"paths": {
|
|
12
|
+
"arlo-sdk": ["../arlo-sdk/src/index.ts"]
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
|
16
|
+
}
|