flowboard-react 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.
Files changed (90) hide show
  1. package/FlowboardReact.podspec +20 -0
  2. package/LICENSE +20 -0
  3. package/README.md +122 -0
  4. package/android/build.gradle +67 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/com/flowboardreact/FlowboardReactModule.kt +15 -0
  7. package/android/src/main/java/com/flowboardreact/FlowboardReactPackage.kt +33 -0
  8. package/ios/FlowboardReact.h +5 -0
  9. package/ios/FlowboardReact.mm +21 -0
  10. package/lib/module/Flowboard.js +167 -0
  11. package/lib/module/Flowboard.js.map +1 -0
  12. package/lib/module/FlowboardProvider.js +52 -0
  13. package/lib/module/FlowboardProvider.js.map +1 -0
  14. package/lib/module/NativeFlowboardReact.js +5 -0
  15. package/lib/module/NativeFlowboardReact.js.map +1 -0
  16. package/lib/module/components/FlowboardFlow.js +389 -0
  17. package/lib/module/components/FlowboardFlow.js.map +1 -0
  18. package/lib/module/components/FlowboardRenderer.js +1684 -0
  19. package/lib/module/components/FlowboardRenderer.js.map +1 -0
  20. package/lib/module/components/widgets/sliderRegistry.js +48 -0
  21. package/lib/module/components/widgets/sliderRegistry.js.map +1 -0
  22. package/lib/module/core/analyticsManager.js +110 -0
  23. package/lib/module/core/analyticsManager.js.map +1 -0
  24. package/lib/module/core/assetPreloader.js +72 -0
  25. package/lib/module/core/assetPreloader.js.map +1 -0
  26. package/lib/module/core/clientContext.js +105 -0
  27. package/lib/module/core/clientContext.js.map +1 -0
  28. package/lib/module/core/fontAwesome.js +110 -0
  29. package/lib/module/core/fontAwesome.js.map +1 -0
  30. package/lib/module/core/onboardingRepository.js +62 -0
  31. package/lib/module/core/onboardingRepository.js.map +1 -0
  32. package/lib/module/core/resolverService.js +58 -0
  33. package/lib/module/core/resolverService.js.map +1 -0
  34. package/lib/module/index.js +5 -0
  35. package/lib/module/index.js.map +1 -0
  36. package/lib/module/package.json +1 -0
  37. package/lib/module/types/flowboard.js +4 -0
  38. package/lib/module/types/flowboard.js.map +1 -0
  39. package/lib/module/types/react-native-vector-icons.d.js +2 -0
  40. package/lib/module/types/react-native-vector-icons.d.js.map +1 -0
  41. package/lib/module/utils/flowboardUtils.js +379 -0
  42. package/lib/module/utils/flowboardUtils.js.map +1 -0
  43. package/lib/typescript/package.json +1 -0
  44. package/lib/typescript/src/Flowboard.d.ts +33 -0
  45. package/lib/typescript/src/Flowboard.d.ts.map +1 -0
  46. package/lib/typescript/src/FlowboardProvider.d.ts +5 -0
  47. package/lib/typescript/src/FlowboardProvider.d.ts.map +1 -0
  48. package/lib/typescript/src/NativeFlowboardReact.d.ts +7 -0
  49. package/lib/typescript/src/NativeFlowboardReact.d.ts.map +1 -0
  50. package/lib/typescript/src/components/FlowboardFlow.d.ts +14 -0
  51. package/lib/typescript/src/components/FlowboardFlow.d.ts.map +1 -0
  52. package/lib/typescript/src/components/FlowboardRenderer.d.ts +31 -0
  53. package/lib/typescript/src/components/FlowboardRenderer.d.ts.map +1 -0
  54. package/lib/typescript/src/components/widgets/sliderRegistry.d.ts +16 -0
  55. package/lib/typescript/src/components/widgets/sliderRegistry.d.ts.map +1 -0
  56. package/lib/typescript/src/core/analyticsManager.d.ts +42 -0
  57. package/lib/typescript/src/core/analyticsManager.d.ts.map +1 -0
  58. package/lib/typescript/src/core/assetPreloader.d.ts +8 -0
  59. package/lib/typescript/src/core/assetPreloader.d.ts.map +1 -0
  60. package/lib/typescript/src/core/clientContext.d.ts +27 -0
  61. package/lib/typescript/src/core/clientContext.d.ts.map +1 -0
  62. package/lib/typescript/src/core/fontAwesome.d.ts +8 -0
  63. package/lib/typescript/src/core/fontAwesome.d.ts.map +1 -0
  64. package/lib/typescript/src/core/onboardingRepository.d.ts +15 -0
  65. package/lib/typescript/src/core/onboardingRepository.d.ts.map +1 -0
  66. package/lib/typescript/src/core/resolverService.d.ts +11 -0
  67. package/lib/typescript/src/core/resolverService.d.ts.map +1 -0
  68. package/lib/typescript/src/index.d.ts +4 -0
  69. package/lib/typescript/src/index.d.ts.map +1 -0
  70. package/lib/typescript/src/types/flowboard.d.ts +34 -0
  71. package/lib/typescript/src/types/flowboard.d.ts.map +1 -0
  72. package/lib/typescript/src/utils/flowboardUtils.d.ts +31 -0
  73. package/lib/typescript/src/utils/flowboardUtils.d.ts.map +1 -0
  74. package/package.json +192 -0
  75. package/src/Flowboard.ts +223 -0
  76. package/src/FlowboardProvider.tsx +60 -0
  77. package/src/NativeFlowboardReact.ts +7 -0
  78. package/src/components/FlowboardFlow.tsx +513 -0
  79. package/src/components/FlowboardRenderer.tsx +1957 -0
  80. package/src/components/widgets/sliderRegistry.tsx +56 -0
  81. package/src/core/analyticsManager.ts +125 -0
  82. package/src/core/assetPreloader.ts +103 -0
  83. package/src/core/clientContext.ts +132 -0
  84. package/src/core/fontAwesome.ts +90 -0
  85. package/src/core/onboardingRepository.ts +79 -0
  86. package/src/core/resolverService.ts +69 -0
  87. package/src/index.tsx +11 -0
  88. package/src/types/flowboard.ts +50 -0
  89. package/src/types/react-native-vector-icons.d.ts +15 -0
  90. package/src/utils/flowboardUtils.ts +400 -0
@@ -0,0 +1,56 @@
1
+ import React, { createContext, useContext } from 'react';
2
+
3
+ export class SliderRegistry {
4
+ private current = new Map<string, number>();
5
+ private counts = new Map<string, number>();
6
+ private listeners = new Map<string, Set<(value: number) => void>>();
7
+
8
+ getNotifier(id: string, callback: (value: number) => void): () => void {
9
+ if (!this.listeners.has(id)) {
10
+ this.listeners.set(id, new Set());
11
+ }
12
+ const set = this.listeners.get(id)!;
13
+ set.add(callback);
14
+ callback(this.getCurrent(id));
15
+ return () => {
16
+ set.delete(callback);
17
+ };
18
+ }
19
+
20
+ update(id: string, page: number, count: number): void {
21
+ this.current.set(id, page);
22
+ this.counts.set(id, count);
23
+ const set = this.listeners.get(id);
24
+ if (set) {
25
+ set.forEach((listener) => listener(page));
26
+ }
27
+ }
28
+
29
+ getPageCount(id: string): number {
30
+ return this.counts.get(id) ?? 0;
31
+ }
32
+
33
+ getCurrent(id: string): number {
34
+ return this.current.get(id) ?? 0;
35
+ }
36
+ }
37
+
38
+ const SliderRegistryContext = createContext<SliderRegistry | null>(null);
39
+
40
+ export function SliderRegistryProvider({
41
+ registry,
42
+ children,
43
+ }: {
44
+ registry: SliderRegistry;
45
+ children: React.ReactNode;
46
+ }) {
47
+ return (
48
+ <SliderRegistryContext.Provider value={registry}>
49
+ {children}
50
+ </SliderRegistryContext.Provider>
51
+ );
52
+ }
53
+
54
+ export function useSliderRegistry(): SliderRegistry | null {
55
+ return useContext(SliderRegistryContext);
56
+ }
@@ -0,0 +1,125 @@
1
+ import { Platform } from 'react-native';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import 'react-native-get-random-values';
4
+ import type { ClientContext } from './clientContext';
5
+
6
+ export enum OnboardingOutcome {
7
+ completed = 'completed',
8
+ dismissed = 'dismissed',
9
+ purchased = 'purchased',
10
+ failed = 'failed',
11
+ }
12
+
13
+ const ANALYTICS_ENDPOINT = 'https://track-638704832888.europe-west1.run.app';
14
+
15
+ export class AnalyticsManager {
16
+ private static _instance = new AnalyticsManager();
17
+ static get instance(): AnalyticsManager {
18
+ return AnalyticsManager._instance;
19
+ }
20
+
21
+ private enabled = true;
22
+ private clientContext: ClientContext | null = null;
23
+ private flowContext: Record<string, any> = {};
24
+
25
+ get clientContextSnapshot(): ClientContext | null {
26
+ return this.clientContext;
27
+ }
28
+
29
+ configure(params: { enabled?: boolean; context: ClientContext }): void {
30
+ this.enabled = params.enabled ?? true;
31
+ this.clientContext = params.context;
32
+ }
33
+
34
+ startSession(params: { flowData: Record<string, any> }): void {
35
+ this.flowContext = { ...params.flowData };
36
+ delete this.flowContext.screens;
37
+ const sessionId = uuidv4();
38
+ this.flowContext.session_id = sessionId;
39
+ }
40
+
41
+ endSession(): void {
42
+ this.flowContext = {};
43
+ }
44
+
45
+ track(eventName: string, properties: Record<string, any>): void {
46
+ if (!this.enabled) return;
47
+ try {
48
+ const eventId = uuidv4();
49
+ const timestamp = new Date().toISOString();
50
+ const context = this.clientContext;
51
+
52
+ const payload = {
53
+ event_id: eventId,
54
+ name: eventName,
55
+ timestamp,
56
+ bundle_id: context?.bundleId,
57
+ flow_id: this.flowContext.flow_id,
58
+ context: {
59
+ user_id: null,
60
+ device_id: context?.installId ?? 'unknown',
61
+ platform: context?.os ?? Platform.OS,
62
+ app_version: context?.appVersion ?? 'unknown',
63
+ sdk_version: '1.0.0',
64
+ locale: context?.locale,
65
+ },
66
+ flow_context: this.flowContext,
67
+ properties,
68
+ };
69
+
70
+ this.transmit(payload);
71
+ } catch {
72
+ // silent
73
+ }
74
+ }
75
+
76
+ private async transmit(payload: Record<string, any>): Promise<void> {
77
+ try {
78
+ const response = await fetch(ANALYTICS_ENDPOINT, {
79
+ method: 'POST',
80
+ headers: { 'Content-Type': 'application/json' },
81
+ body: JSON.stringify(payload),
82
+ });
83
+ if (!response.ok) {
84
+ // swallow
85
+ }
86
+ } catch {
87
+ // swallow
88
+ }
89
+ }
90
+
91
+ trackOnboardLoaded(params: { durationMs: number; cached: boolean }): void {
92
+ this.track('onboard_loaded', {
93
+ duration_ms: params.durationMs,
94
+ cached: params.cached,
95
+ });
96
+ }
97
+
98
+ trackOnboardStarted(): void {
99
+ this.track('onboard_started', {});
100
+ }
101
+
102
+ trackScreenView(params: {
103
+ stepId: string;
104
+ stepIndex: number;
105
+ stepName?: string | null;
106
+ }): void {
107
+ this.track('screen_view', {
108
+ step_id: params.stepId,
109
+ step_index: params.stepIndex,
110
+ ...(params.stepName ? { step_name: params.stepName } : {}),
111
+ });
112
+ }
113
+
114
+ trackOnboardEnded(params: {
115
+ outcome: OnboardingOutcome;
116
+ totalDurationMs: number;
117
+ finalStepId: string;
118
+ }): void {
119
+ this.track('onboard_ended', {
120
+ outcome: params.outcome,
121
+ total_duration_ms: params.totalDurationMs,
122
+ final_step_id: params.finalStepId,
123
+ });
124
+ }
125
+ }
@@ -0,0 +1,103 @@
1
+ import { Image } from 'react-native';
2
+
3
+ export class AssetPreloader {
4
+ private preloadedUrls = new Set<string>();
5
+
6
+ async preloadScreenAssets(screenData: Record<string, any>): Promise<void> {
7
+ const assets = this.extractAssets(screenData);
8
+ if (
9
+ assets.networkImages.length === 0 &&
10
+ assets.networkLotties.length === 0
11
+ ) {
12
+ return;
13
+ }
14
+
15
+ const tasks: Promise<any>[] = [];
16
+
17
+ for (const url of assets.networkImages) {
18
+ if (this.preloadedUrls.has(url)) continue;
19
+ this.preloadedUrls.add(url);
20
+ tasks.push(Image.prefetch(url).catch(() => null));
21
+ }
22
+
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
+ if (tasks.length > 0) {
34
+ await Promise.all(tasks);
35
+ }
36
+ }
37
+
38
+ clear(): void {
39
+ this.preloadedUrls.clear();
40
+ }
41
+
42
+ private extractAssets(data: Record<string, any>): {
43
+ networkImages: string[];
44
+ networkLotties: string[];
45
+ } {
46
+ const assets = {
47
+ networkImages: [] as string[],
48
+ networkLotties: [] as string[],
49
+ };
50
+ this.scanWidget(data, assets, 0);
51
+ return assets;
52
+ }
53
+
54
+ private scanWidget(
55
+ widget: any,
56
+ assets: { networkImages: string[]; networkLotties: string[] },
57
+ depth: number
58
+ ): void {
59
+ if (!widget || depth > 50) return;
60
+
61
+ if (Array.isArray(widget)) {
62
+ widget.forEach((item) => this.scanWidget(item, assets, depth));
63
+ return;
64
+ }
65
+
66
+ if (typeof widget !== 'object') return;
67
+
68
+ const type = widget.type;
69
+ const props = widget.properties ?? widget;
70
+
71
+ if (type === 'image') {
72
+ const source = props.source;
73
+ const url = props.url;
74
+ if (source === 'network' && url) {
75
+ assets.networkImages.push(url);
76
+ }
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);
84
+ }
85
+ }
86
+
87
+ 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);
95
+ }
96
+
97
+ Object.values(widget).forEach((value) => {
98
+ if (typeof value === 'object') {
99
+ this.scanWidget(value, assets, depth + 1);
100
+ }
101
+ });
102
+ }
103
+ }
@@ -0,0 +1,132 @@
1
+ import AsyncStorage from '@react-native-async-storage/async-storage';
2
+ import DeviceInfo from 'react-native-device-info';
3
+ import { Platform } from 'react-native';
4
+ import { v4 as uuidv4 } from 'uuid';
5
+ import 'react-native-get-random-values';
6
+
7
+ export type ClientContextData = {
8
+ appVersion: string;
9
+ buildNumber: string;
10
+ locale: string;
11
+ country: string;
12
+ os: string;
13
+ osVersion: string;
14
+ deviceType: string;
15
+ installId: string;
16
+ bundleId: string;
17
+ };
18
+
19
+ const INSTALL_ID_KEY = 'flowboard_install_id';
20
+
21
+ export class ClientContext {
22
+ appVersion: string;
23
+ buildNumber: string;
24
+ locale: string;
25
+ country: string;
26
+ os: string;
27
+ osVersion: string;
28
+ deviceType: string;
29
+ installId: string;
30
+ bundleId: string;
31
+
32
+ private constructor(data: ClientContextData) {
33
+ this.appVersion = data.appVersion;
34
+ this.buildNumber = data.buildNumber;
35
+ this.locale = data.locale;
36
+ this.country = data.country;
37
+ this.os = data.os;
38
+ this.osVersion = data.osVersion;
39
+ this.deviceType = data.deviceType;
40
+ this.installId = data.installId;
41
+ this.bundleId = data.bundleId;
42
+ }
43
+
44
+ static async create(): Promise<ClientContext> {
45
+ const installId = await getOrCreateInstallId();
46
+
47
+ const appVersion = DeviceInfo.getVersion();
48
+ const buildNumber = DeviceInfo.getBuildNumber();
49
+ const bundleId = DeviceInfo.getBundleId();
50
+
51
+ const locale = await getDeviceLocale();
52
+ const country = locale.includes('_') ? locale.split('_').pop() ?? '' : '';
53
+
54
+ const os = Platform.OS;
55
+ const osVersion = DeviceInfo.getSystemVersion();
56
+ const deviceType = mapDeviceType(await DeviceInfo.getDeviceType());
57
+
58
+ return new ClientContext({
59
+ appVersion,
60
+ buildNumber,
61
+ locale,
62
+ country,
63
+ os,
64
+ osVersion,
65
+ deviceType,
66
+ installId,
67
+ bundleId,
68
+ });
69
+ }
70
+
71
+ toJson(): Record<string, any> {
72
+ return {
73
+ app: {
74
+ version: this.appVersion,
75
+ build: this.buildNumber,
76
+ bundleId: this.bundleId,
77
+ },
78
+ user: { locale: this.locale, country: this.country },
79
+ platform: {
80
+ os: this.os,
81
+ osVersion: this.osVersion,
82
+ deviceType: this.deviceType,
83
+ },
84
+ device: { installId: this.installId },
85
+ };
86
+ }
87
+ }
88
+
89
+ async function getOrCreateInstallId(): Promise<string> {
90
+ const stored = await AsyncStorage.getItem(INSTALL_ID_KEY);
91
+ if (stored) return stored;
92
+ const installId = uuidv4();
93
+ await AsyncStorage.setItem(INSTALL_ID_KEY, installId);
94
+ return installId;
95
+ }
96
+
97
+ async function getDeviceLocale(): Promise<string> {
98
+ const deviceInfoAny = DeviceInfo as any;
99
+ if (typeof deviceInfoAny.getDeviceLocale === 'function') {
100
+ const locale = await deviceInfoAny.getDeviceLocale();
101
+ if (locale) return locale;
102
+ }
103
+ if (typeof deviceInfoAny.getDeviceLocales === 'function') {
104
+ const locales = await deviceInfoAny.getDeviceLocales();
105
+ if (Array.isArray(locales) && locales.length > 0) {
106
+ const first = locales[0];
107
+ if (typeof first === 'string') return first;
108
+ if (first?.languageTag) return first.languageTag;
109
+ if (first?.languageCode) {
110
+ return first.countryCode
111
+ ? `${first.languageCode}_${first.countryCode}`
112
+ : first.languageCode;
113
+ }
114
+ }
115
+ }
116
+ return 'en_US';
117
+ }
118
+
119
+ function mapDeviceType(deviceType: string): string {
120
+ switch (deviceType?.toLowerCase()) {
121
+ case 'handset':
122
+ return 'phone';
123
+ case 'tablet':
124
+ return 'tablet';
125
+ case 'desktop':
126
+ return 'desktop';
127
+ case 'tv':
128
+ return 'desktop';
129
+ default:
130
+ return 'phone';
131
+ }
132
+ }
@@ -0,0 +1,90 @@
1
+ import FontAwesome6 from 'react-native-vector-icons/FontAwesome6';
2
+
3
+ const hexRegex = /^(0x)?[a-fA-F0-9]{4,5}$/;
4
+
5
+ export type FontAwesomeStyle =
6
+ | 'brands'
7
+ | 'regular'
8
+ | 'solid'
9
+ | 'light'
10
+ | 'thin'
11
+ | 'duotone'
12
+ | 'sharpthin'
13
+ | 'sharplight'
14
+ | 'sharpregular'
15
+ | 'sharpsolid';
16
+
17
+ export function resolveFontAwesomeIcon(
18
+ name?: string,
19
+ style: string = 'solid'
20
+ ): { name: string | null; props: Record<string, any> } {
21
+ if (!name) return { name: null, props: styleProps(style) };
22
+
23
+ if (hexRegex.test(name)) {
24
+ const codePoint = parseInt(name.replace(/^0x/, ''), 16);
25
+ const glyphMap = getGlyphMap();
26
+ if (glyphMap) {
27
+ let fallback: string | null = null;
28
+ for (const [iconName, value] of Object.entries(glyphMap)) {
29
+ if (value === codePoint) {
30
+ fallback = iconName;
31
+ if (style === 'solid' && iconName.toLowerCase().startsWith('solid')) {
32
+ return { name: iconName, props: styleProps(style) };
33
+ }
34
+ if (
35
+ style === 'regular' &&
36
+ iconName.toLowerCase().startsWith('regular')
37
+ ) {
38
+ return { name: iconName, props: styleProps(style) };
39
+ }
40
+ if (style === 'brands' && iconName.toLowerCase().includes('brand')) {
41
+ return { name: iconName, props: styleProps(style) };
42
+ }
43
+ }
44
+ }
45
+ if (fallback) return { name: fallback, props: styleProps(style) };
46
+ }
47
+ }
48
+
49
+ return { name, props: styleProps(style) };
50
+ }
51
+
52
+ function getGlyphMap(): Record<string, number> | null {
53
+ const anyIcon = FontAwesome6 as any;
54
+ if (typeof anyIcon.getRawGlyphMap === 'function') {
55
+ return anyIcon.getRawGlyphMap();
56
+ }
57
+ if (anyIcon.glyphMap) {
58
+ return anyIcon.glyphMap as Record<string, number>;
59
+ }
60
+ return null;
61
+ }
62
+
63
+ function styleProps(style: string): Record<string, any> {
64
+ const normalized = style.toLowerCase().replace(/-/g, '');
65
+ switch (normalized) {
66
+ case 'brands':
67
+ return { brand: true };
68
+ case 'regular':
69
+ return { regular: true };
70
+ case 'light':
71
+ return { light: true };
72
+ case 'thin':
73
+ return { thin: true };
74
+ case 'duotone':
75
+ return { duotone: true };
76
+ case 'sharpthin':
77
+ return { sharp: true, thin: true };
78
+ case 'sharplight':
79
+ return { sharp: true, light: true };
80
+ case 'sharpregular':
81
+ return { sharp: true, regular: true };
82
+ case 'sharpsolid':
83
+ return { sharp: true, solid: true };
84
+ case 'solid':
85
+ default:
86
+ return { solid: true };
87
+ }
88
+ }
89
+
90
+ export { FontAwesome6 };
@@ -0,0 +1,79 @@
1
+ import AsyncStorage from '@react-native-async-storage/async-storage';
2
+ import type { FlowboardData } from '../types/flowboard';
3
+
4
+ const STORAGE_KEY = 'flowboard_onboarding_json';
5
+ const FETCH_TIME_KEY = 'flowboard_fetch_time';
6
+ const PROGRESS_FLOW_KEY = 'flowboard_progress_flow_id';
7
+ const PROGRESS_STEP_KEY = 'flowboard_progress_step';
8
+ const PROGRESS_FORM_DATA_KEY = 'flowboard_progress_form_data';
9
+
10
+ export class OnboardingRepository {
11
+ async saveOnboardingJson(json: FlowboardData): Promise<void> {
12
+ await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(json));
13
+ await AsyncStorage.setItem(FETCH_TIME_KEY, Date.now().toString());
14
+ }
15
+
16
+ async getOnboardingJson(): Promise<FlowboardData | null> {
17
+ const jsonString = await AsyncStorage.getItem(STORAGE_KEY);
18
+ if (!jsonString) return null;
19
+ try {
20
+ return JSON.parse(jsonString) as FlowboardData;
21
+ } catch {
22
+ await this.clearOnboardingJson();
23
+ return null;
24
+ }
25
+ }
26
+
27
+ async clearOnboardingJson(): Promise<void> {
28
+ await AsyncStorage.multiRemove([STORAGE_KEY, FETCH_TIME_KEY]);
29
+ }
30
+
31
+ async saveProgress(params: {
32
+ flowId: string;
33
+ stepIndex: number;
34
+ formData: Record<string, any>;
35
+ }): Promise<void> {
36
+ await AsyncStorage.setItem(PROGRESS_FLOW_KEY, params.flowId);
37
+ await AsyncStorage.setItem(PROGRESS_STEP_KEY, params.stepIndex.toString());
38
+ await AsyncStorage.setItem(
39
+ PROGRESS_FORM_DATA_KEY,
40
+ JSON.stringify(params.formData)
41
+ );
42
+ }
43
+
44
+ async getProgressStepForFlow(flowId: string): Promise<number | null> {
45
+ const storedFlowId = await AsyncStorage.getItem(PROGRESS_FLOW_KEY);
46
+ if (storedFlowId !== flowId) return null;
47
+ const value = await AsyncStorage.getItem(PROGRESS_STEP_KEY);
48
+ if (!value) return null;
49
+ const parsed = Number(value);
50
+ return Number.isNaN(parsed) ? null : parsed;
51
+ }
52
+
53
+ async getProgressFormDataForFlow(
54
+ flowId: string
55
+ ): Promise<Record<string, any> | null> {
56
+ const storedFlowId = await AsyncStorage.getItem(PROGRESS_FLOW_KEY);
57
+ if (storedFlowId !== flowId) return null;
58
+ const jsonString = await AsyncStorage.getItem(PROGRESS_FORM_DATA_KEY);
59
+ if (!jsonString) return null;
60
+ try {
61
+ const decoded = JSON.parse(jsonString);
62
+ if (decoded && typeof decoded === 'object') {
63
+ return { ...decoded } as Record<string, any>;
64
+ }
65
+ return null;
66
+ } catch {
67
+ await this.clearProgress();
68
+ return null;
69
+ }
70
+ }
71
+
72
+ async clearProgress(): Promise<void> {
73
+ await AsyncStorage.multiRemove([
74
+ PROGRESS_FLOW_KEY,
75
+ PROGRESS_STEP_KEY,
76
+ PROGRESS_FORM_DATA_KEY,
77
+ ]);
78
+ }
79
+ }
@@ -0,0 +1,69 @@
1
+ import type { ClientContext } from './clientContext';
2
+ import type { FlowboardData } from '../types/flowboard';
3
+
4
+ const DEFAULT_ENDPOINT = 'https://test-638704832888.europe-west1.run.app';
5
+
6
+ export class ResolverService {
7
+ private endpoint: string;
8
+
9
+ constructor(endpoint?: string) {
10
+ this.endpoint = endpoint ?? DEFAULT_ENDPOINT;
11
+ }
12
+
13
+ async fetchOnboardingJson(params: {
14
+ context: ClientContext;
15
+ appId: string;
16
+ }): Promise<FlowboardData> {
17
+ const { context, appId } = params;
18
+ const payload = context.toJson();
19
+ payload.appId = appId;
20
+ payload.installId = context.installId;
21
+ payload.bundleId = context.bundleId;
22
+
23
+ payload.appVersion = context.appVersion;
24
+ payload.buildNumber = context.buildNumber;
25
+ payload.os = context.os;
26
+ payload.osVersion = context.osVersion;
27
+ payload.deviceType = context.deviceType;
28
+ payload.locale = context.locale;
29
+ payload.country = context.country;
30
+
31
+ payload.sdk = {
32
+ name: 'flowboard',
33
+ version: '1.0.0',
34
+ };
35
+
36
+ let response: Response;
37
+ try {
38
+ response = await fetch(this.endpoint, {
39
+ method: 'POST',
40
+ headers: { 'Content-Type': 'application/json' },
41
+ body: JSON.stringify(payload),
42
+ });
43
+ } catch (error) {
44
+ throw new Error(`Failed to connect to resolver: ${String(error)}`);
45
+ }
46
+
47
+ if (!response.ok) {
48
+ const body = await response.text();
49
+ throw new Error(
50
+ `Failed to load onboarding config: ${response.status} ${body}`
51
+ );
52
+ }
53
+
54
+ const body = (await response.json()) as FlowboardData;
55
+
56
+ const flowId = response.headers.get('x-flowboard-flow-id');
57
+ if (flowId) body.flow_id = flowId;
58
+ const variantId = response.headers.get('x-flowboard-variant-id');
59
+ if (variantId) body.variant_id = variantId;
60
+ const audienceId = response.headers.get('x-flowboard-audience-id');
61
+ if (audienceId) body.audience_id = audienceId;
62
+ const bucket = response.headers.get('x-flowboard-bucket');
63
+ if (bucket) body.bucket = bucket;
64
+ const experimentId = response.headers.get('x-flowboard-experiment-id');
65
+ if (experimentId) body.experiment_id = experimentId;
66
+
67
+ return body;
68
+ }
69
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,11 @@
1
+ export { Flowboard } from './Flowboard';
2
+ export { default as FlowboardProvider } from './FlowboardProvider';
3
+ export type {
4
+ FlowboardContext,
5
+ CustomScreenBuilder,
6
+ CustomActionBuilder,
7
+ OnboardingEndCallback,
8
+ OnStepChangeCallback,
9
+ FlowboardLaunchOptions,
10
+ FlowboardData,
11
+ } from './types/flowboard';