flowboard-react 0.2.0 → 0.4.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.
Files changed (45) hide show
  1. package/README.md +15 -2
  2. package/lib/module/Flowboard.js +50 -11
  3. package/lib/module/Flowboard.js.map +1 -1
  4. package/lib/module/FlowboardProvider.js +18 -14
  5. package/lib/module/FlowboardProvider.js.map +1 -1
  6. package/lib/module/components/FlowboardFlow.js +70 -37
  7. package/lib/module/components/FlowboardFlow.js.map +1 -1
  8. package/lib/module/components/FlowboardRenderer.js +622 -105
  9. package/lib/module/components/FlowboardRenderer.js.map +1 -1
  10. package/lib/module/core/assetPreloader.js +20 -18
  11. package/lib/module/core/assetPreloader.js.map +1 -1
  12. package/lib/module/core/resolverService.js +31 -1
  13. package/lib/module/core/resolverService.js.map +1 -1
  14. package/lib/module/index.js +1 -0
  15. package/lib/module/index.js.map +1 -1
  16. package/lib/module/types/react-native-peers.d.js +2 -0
  17. package/lib/module/types/react-native-peers.d.js.map +1 -0
  18. package/lib/module/utils/flowboardUtils.js +20 -14
  19. package/lib/module/utils/flowboardUtils.js.map +1 -1
  20. package/lib/typescript/src/Flowboard.d.ts +5 -1
  21. package/lib/typescript/src/Flowboard.d.ts.map +1 -1
  22. package/lib/typescript/src/FlowboardProvider.d.ts.map +1 -1
  23. package/lib/typescript/src/components/FlowboardFlow.d.ts +1 -2
  24. package/lib/typescript/src/components/FlowboardFlow.d.ts.map +1 -1
  25. package/lib/typescript/src/components/FlowboardRenderer.d.ts.map +1 -1
  26. package/lib/typescript/src/core/assetPreloader.d.ts.map +1 -1
  27. package/lib/typescript/src/core/resolverService.d.ts +6 -0
  28. package/lib/typescript/src/core/resolverService.d.ts.map +1 -1
  29. package/lib/typescript/src/index.d.ts +3 -1
  30. package/lib/typescript/src/index.d.ts.map +1 -1
  31. package/lib/typescript/src/types/flowboard.d.ts +4 -0
  32. package/lib/typescript/src/types/flowboard.d.ts.map +1 -1
  33. package/lib/typescript/src/utils/flowboardUtils.d.ts +6 -0
  34. package/lib/typescript/src/utils/flowboardUtils.d.ts.map +1 -1
  35. package/package.json +4 -3
  36. package/src/Flowboard.ts +86 -16
  37. package/src/FlowboardProvider.tsx +20 -16
  38. package/src/components/FlowboardFlow.tsx +89 -49
  39. package/src/components/FlowboardRenderer.tsx +771 -98
  40. package/src/core/assetPreloader.ts +21 -32
  41. package/src/core/resolverService.ts +42 -2
  42. package/src/index.tsx +3 -0
  43. package/src/types/flowboard.ts +5 -0
  44. package/src/types/react-native-peers.d.ts +106 -0
  45. package/src/utils/flowboardUtils.ts +28 -14
@@ -1,14 +1,18 @@
1
1
  import { Image } from 'react-native';
2
2
 
3
+ function isNetworkLikeUri(value: unknown): value is string {
4
+ if (typeof value !== 'string') return false;
5
+ const normalized = value.trim();
6
+ if (!normalized) return false;
7
+ return /^(https?:|file:|content:|data:)/i.test(normalized);
8
+ }
9
+
3
10
  export class AssetPreloader {
4
11
  private preloadedUrls = new Set<string>();
5
12
 
6
13
  async preloadScreenAssets(screenData: Record<string, any>): Promise<void> {
7
14
  const assets = this.extractAssets(screenData);
8
- if (
9
- assets.networkImages.length === 0 &&
10
- assets.networkLotties.length === 0
11
- ) {
15
+ if (assets.networkImages.length === 0) {
12
16
  return;
13
17
  }
14
18
 
@@ -20,16 +24,6 @@ export class AssetPreloader {
20
24
  tasks.push(Image.prefetch(url).catch(() => null));
21
25
  }
22
26
 
23
- for (const url of assets.networkLotties) {
24
- if (this.preloadedUrls.has(url)) continue;
25
- this.preloadedUrls.add(url);
26
- tasks.push(
27
- fetch(url)
28
- .then(() => null)
29
- .catch(() => null)
30
- );
31
- }
32
-
33
27
  if (tasks.length > 0) {
34
28
  await Promise.all(tasks);
35
29
  }
@@ -41,11 +35,9 @@ export class AssetPreloader {
41
35
 
42
36
  private extractAssets(data: Record<string, any>): {
43
37
  networkImages: string[];
44
- networkLotties: string[];
45
38
  } {
46
39
  const assets = {
47
40
  networkImages: [] as string[],
48
- networkLotties: [] as string[],
49
41
  };
50
42
  this.scanWidget(data, assets, 0);
51
43
  return assets;
@@ -53,7 +45,7 @@ export class AssetPreloader {
53
45
 
54
46
  private scanWidget(
55
47
  widget: any,
56
- assets: { networkImages: string[]; networkLotties: string[] },
48
+ assets: { networkImages: string[] },
57
49
  depth: number
58
50
  ): void {
59
51
  if (!widget || depth > 50) return;
@@ -72,26 +64,23 @@ export class AssetPreloader {
72
64
  const source = props.source;
73
65
  const url = props.url;
74
66
  if (source === 'network' && url) {
75
- assets.networkImages.push(url);
67
+ if (isNetworkLikeUri(url)) assets.networkImages.push(url);
76
68
  }
77
- }
78
-
79
- if (type === 'lottie') {
80
- const source = props.source;
81
- const url = props.url;
82
- if (source === 'network' && url) {
83
- assets.networkLotties.push(url);
69
+ if (source === 'asset' && props.path) {
70
+ if (isNetworkLikeUri(props.path)) assets.networkImages.push(props.path);
84
71
  }
85
72
  }
86
73
 
87
74
  const background = widget.background ?? props.background;
88
- if (
89
- background &&
90
- typeof background === 'object' &&
91
- background.type === 'image'
92
- ) {
93
- const url = background.url;
94
- if (url) assets.networkImages.push(url);
75
+ if (background && typeof background === 'object') {
76
+ if (background.type === 'image') {
77
+ const url = background.url;
78
+ if (isNetworkLikeUri(url)) assets.networkImages.push(url);
79
+ }
80
+ if (background.type === 'asset') {
81
+ const path = background.path;
82
+ if (isNetworkLikeUri(path)) assets.networkImages.push(path);
83
+ }
95
84
  }
96
85
 
97
86
  Object.values(widget).forEach((value) => {
@@ -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
@@ -1,5 +1,6 @@
1
1
  export { Flowboard } from './Flowboard';
2
2
  export { default as FlowboardProvider } from './FlowboardProvider';
3
+ export { default as FlowboardFlow } from './components/FlowboardFlow';
3
4
  export type {
4
5
  FlowboardContext,
5
6
  CustomScreenBuilder,
@@ -7,5 +8,7 @@ export type {
7
8
  OnboardingEndCallback,
8
9
  OnStepChangeCallback,
9
10
  FlowboardLaunchOptions,
11
+ FlowboardLaunchByIdOptions,
10
12
  FlowboardData,
11
13
  } from './types/flowboard';
14
+ export type { FlowboardFlowProps } from './components/FlowboardFlow';
@@ -47,4 +47,9 @@ export type FlowboardLaunchOptions = {
47
47
  onStepChange?: OnStepChangeCallback;
48
48
  enableAnalytics?: boolean;
49
49
  alwaysRestart?: boolean;
50
+ resumeProgress?: boolean;
51
+ };
52
+
53
+ export type FlowboardLaunchByIdOptions = FlowboardLaunchOptions & {
54
+ locale?: string;
50
55
  };
@@ -0,0 +1,106 @@
1
+ declare module '@react-native-async-storage/async-storage' {
2
+ const AsyncStorage: any;
3
+ export default AsyncStorage;
4
+ }
5
+
6
+ declare module '@react-native-masked-view/masked-view' {
7
+ import type { ComponentType } from 'react';
8
+ const MaskedView: ComponentType<any>;
9
+ export default MaskedView;
10
+ }
11
+
12
+ declare module 'lottie-react-native' {
13
+ import type { ComponentType } from 'react';
14
+ const LottieView: ComponentType<any>;
15
+ export default LottieView;
16
+ }
17
+
18
+ declare module 'react-native-device-info' {
19
+ const DeviceInfo: any;
20
+ export default DeviceInfo;
21
+ }
22
+
23
+ declare module 'react-native-in-app-review' {
24
+ const InAppReview: {
25
+ isAvailable: () => boolean;
26
+ RequestInAppReview: () => Promise<boolean>;
27
+ };
28
+ export default InAppReview;
29
+ }
30
+
31
+ declare module 'react-native-linear-gradient' {
32
+ import type { ComponentType } from 'react';
33
+ const LinearGradient: ComponentType<any>;
34
+ export default LinearGradient;
35
+ }
36
+
37
+ declare module 'react-native-mask-input' {
38
+ import type { ComponentType } from 'react';
39
+ const MaskInput: ComponentType<any>;
40
+ export default MaskInput;
41
+ }
42
+
43
+ declare module 'react-native-pager-view' {
44
+ import { Component } from 'react';
45
+
46
+ export type PagerViewOnPageSelectedEvent = {
47
+ nativeEvent: {
48
+ position: number;
49
+ };
50
+ };
51
+
52
+ export default class PagerView extends Component<any> {
53
+ setPage(index: number): void;
54
+ setPageWithoutAnimation(index: number): void;
55
+ }
56
+ }
57
+
58
+ declare module 'react-native-permissions' {
59
+ export const PERMISSIONS: Record<string, any>;
60
+ export const RESULTS: Record<string, string>;
61
+ export function request(permission: string): Promise<string>;
62
+ export function requestNotifications(
63
+ options?: string[]
64
+ ): Promise<{ status: string }>;
65
+ export function openSettings(): Promise<void>;
66
+ }
67
+
68
+ declare module 'react-native-safe-area-context' {
69
+ import type { ComponentType } from 'react';
70
+ export const SafeAreaProvider: ComponentType<any>;
71
+ export const SafeAreaView: ComponentType<any>;
72
+ export const initialWindowMetrics: {
73
+ insets: {
74
+ top: number;
75
+ right: number;
76
+ bottom: number;
77
+ left: number;
78
+ };
79
+ } | null;
80
+ export function useSafeAreaInsets(): {
81
+ top: number;
82
+ right: number;
83
+ bottom: number;
84
+ left: number;
85
+ };
86
+ }
87
+
88
+ declare module 'react-native-svg' {
89
+ import type { ComponentType } from 'react';
90
+
91
+ const Svg: ComponentType<any>;
92
+ export default Svg;
93
+
94
+ export const Circle: ComponentType<any>;
95
+ export const Defs: ComponentType<any>;
96
+ export const G: ComponentType<any>;
97
+ export const Line: ComponentType<any>;
98
+ export const Path: ComponentType<any>;
99
+ export const Polygon: ComponentType<any>;
100
+ export const Rect: ComponentType<any>;
101
+ export const Stop: ComponentType<any>;
102
+ export const Text: ComponentType<any>;
103
+ export const TSpan: ComponentType<any>;
104
+ export const Use: ComponentType<any>;
105
+ export const LinearGradient: ComponentType<any>;
106
+ }
@@ -102,21 +102,21 @@ export function parseInsets(value: any): Insets {
102
102
  return { top: value, right: value, bottom: value, left: value };
103
103
  }
104
104
  if (value && typeof value === 'object') {
105
- if ('horizontal' in value || 'vertical' in value) {
106
- const horizontal = Number(value.horizontal ?? 0);
107
- const vertical = Number(value.vertical ?? 0);
108
- return {
109
- top: vertical,
110
- right: horizontal,
111
- bottom: vertical,
112
- left: horizontal,
113
- };
114
- }
105
+ const horizontal = Number(value.horizontal ?? 0);
106
+ const vertical = Number(value.vertical ?? 0);
107
+ const hasHorizontal = 'horizontal' in value;
108
+ const hasVertical = 'vertical' in value;
109
+
110
+ const top = Number(value.top ?? (hasVertical ? vertical : 0));
111
+ const right = Number(value.right ?? (hasHorizontal ? horizontal : 0));
112
+ const bottom = Number(value.bottom ?? (hasVertical ? vertical : 0));
113
+ const left = Number(value.left ?? (hasHorizontal ? horizontal : 0));
114
+
115
115
  return {
116
- top: Number(value.top ?? 0),
117
- right: Number(value.right ?? 0),
118
- bottom: Number(value.bottom ?? 0),
119
- left: Number(value.left ?? 0),
116
+ top,
117
+ right,
118
+ bottom,
119
+ left,
120
120
  };
121
121
  }
122
122
  return { ...EMPTY_INSETS };
@@ -136,6 +136,20 @@ export function insetsToStyle(insets: Insets): {
136
136
  };
137
137
  }
138
138
 
139
+ export function insetsToMarginStyle(insets: Insets): {
140
+ marginTop: number;
141
+ marginRight: number;
142
+ marginBottom: number;
143
+ marginLeft: number;
144
+ } {
145
+ return {
146
+ marginTop: insets.top,
147
+ marginRight: insets.right,
148
+ marginBottom: insets.bottom,
149
+ marginLeft: insets.left,
150
+ };
151
+ }
152
+
139
153
  export function formatDate(pattern: string, date = new Date()): string {
140
154
  const pad = (value: number, len = 2) => value.toString().padStart(len, '0');
141
155
  const replacements: Record<string, string> = {