flowboard-react 0.6.2 → 0.6.3

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 (88) hide show
  1. package/README.md +168 -52
  2. package/app.plugin.js +341 -0
  3. package/bin/setup.js +427 -0
  4. package/lib/module/FlowboardProvider.js +1 -1
  5. package/lib/module/FlowboardProvider.js.map +1 -1
  6. package/lib/module/components/FlowboardFlow.js +6 -6
  7. package/lib/module/components/FlowboardFlow.js.map +1 -1
  8. package/lib/module/components/FlowboardRenderer.js +6 -6
  9. package/lib/module/components/FlowboardRenderer.js.map +1 -1
  10. package/lib/module/core/clientContext.js +10 -29
  11. package/lib/module/core/clientContext.js.map +1 -1
  12. package/lib/module/core/fontAwesome.js +2 -9
  13. package/lib/module/core/fontAwesome.js.map +1 -1
  14. package/lib/module/core/onboardingRepository.js +13 -13
  15. package/lib/module/core/onboardingRepository.js.map +1 -1
  16. package/lib/module/native/asyncStorage.js +55 -0
  17. package/lib/module/native/asyncStorage.js.map +1 -0
  18. package/lib/module/native/deviceInfo.js +46 -0
  19. package/lib/module/native/deviceInfo.js.map +1 -0
  20. package/lib/module/native/inAppReview.js +17 -0
  21. package/lib/module/native/inAppReview.js.map +1 -0
  22. package/lib/module/native/linearGradient.js +29 -0
  23. package/lib/module/native/linearGradient.js.map +1 -0
  24. package/lib/module/native/lottie.js +20 -0
  25. package/lib/module/native/lottie.js.map +1 -0
  26. package/lib/module/native/maskedView.js +26 -0
  27. package/lib/module/native/maskedView.js.map +1 -0
  28. package/lib/module/native/pagerView.js +79 -0
  29. package/lib/module/native/pagerView.js.map +1 -0
  30. package/lib/module/native/permissions.js +30 -0
  31. package/lib/module/native/permissions.js.map +1 -0
  32. package/lib/module/native/runtime.js +81 -0
  33. package/lib/module/native/runtime.js.map +1 -0
  34. package/lib/module/native/safeAreaContext.js +43 -0
  35. package/lib/module/native/safeAreaContext.js.map +1 -0
  36. package/lib/module/native/svg.js +83 -0
  37. package/lib/module/native/svg.js.map +1 -0
  38. package/lib/module/native/vectorIcons.js +41 -0
  39. package/lib/module/native/vectorIcons.js.map +1 -0
  40. package/lib/typescript/src/components/FlowboardFlow.d.ts.map +1 -1
  41. package/lib/typescript/src/components/FlowboardRenderer.d.ts.map +1 -1
  42. package/lib/typescript/src/core/clientContext.d.ts.map +1 -1
  43. package/lib/typescript/src/core/fontAwesome.d.ts +1 -1
  44. package/lib/typescript/src/core/fontAwesome.d.ts.map +1 -1
  45. package/lib/typescript/src/core/onboardingRepository.d.ts.map +1 -1
  46. package/lib/typescript/src/native/asyncStorage.d.ts +16 -0
  47. package/lib/typescript/src/native/asyncStorage.d.ts.map +1 -0
  48. package/lib/typescript/src/native/deviceInfo.d.ts +11 -0
  49. package/lib/typescript/src/native/deviceInfo.d.ts.map +1 -0
  50. package/lib/typescript/src/native/inAppReview.d.ts +4 -0
  51. package/lib/typescript/src/native/inAppReview.d.ts.map +1 -0
  52. package/lib/typescript/src/native/linearGradient.d.ts +3 -0
  53. package/lib/typescript/src/native/linearGradient.d.ts.map +1 -0
  54. package/lib/typescript/src/native/lottie.d.ts +3 -0
  55. package/lib/typescript/src/native/lottie.d.ts.map +1 -0
  56. package/lib/typescript/src/native/maskedView.d.ts +3 -0
  57. package/lib/typescript/src/native/maskedView.d.ts.map +1 -0
  58. package/lib/typescript/src/native/pagerView.d.ts +14 -0
  59. package/lib/typescript/src/native/pagerView.d.ts.map +1 -0
  60. package/lib/typescript/src/native/permissions.d.ts +9 -0
  61. package/lib/typescript/src/native/permissions.d.ts.map +1 -0
  62. package/lib/typescript/src/native/runtime.d.ts +13 -0
  63. package/lib/typescript/src/native/runtime.d.ts.map +1 -0
  64. package/lib/typescript/src/native/safeAreaContext.d.ts +15 -0
  65. package/lib/typescript/src/native/safeAreaContext.d.ts.map +1 -0
  66. package/lib/typescript/src/native/svg.d.ts +8 -0
  67. package/lib/typescript/src/native/svg.d.ts.map +1 -0
  68. package/lib/typescript/src/native/vectorIcons.d.ts +4 -0
  69. package/lib/typescript/src/native/vectorIcons.d.ts.map +1 -0
  70. package/package.json +20 -14
  71. package/src/FlowboardProvider.tsx +1 -1
  72. package/src/components/FlowboardFlow.tsx +10 -7
  73. package/src/components/FlowboardRenderer.tsx +8 -13
  74. package/src/core/clientContext.ts +10 -32
  75. package/src/core/fontAwesome.ts +2 -9
  76. package/src/core/onboardingRepository.ts +18 -13
  77. package/src/native/asyncStorage.ts +99 -0
  78. package/src/native/deviceInfo.ts +88 -0
  79. package/src/native/inAppReview.ts +34 -0
  80. package/src/native/linearGradient.tsx +24 -0
  81. package/src/native/lottie.tsx +17 -0
  82. package/src/native/maskedView.tsx +19 -0
  83. package/src/native/pagerView.tsx +95 -0
  84. package/src/native/permissions.ts +59 -0
  85. package/src/native/runtime.ts +110 -0
  86. package/src/native/safeAreaContext.tsx +44 -0
  87. package/src/native/svg.tsx +82 -0
  88. package/src/native/vectorIcons.tsx +50 -0
package/README.md CHANGED
@@ -1,42 +1,137 @@
1
1
  # flowboard-react
2
2
 
3
- Flowboard React Native SDK. JSON-driven onboarding flows with custom screens and full SDUI rendering parity with the Flutter package.
3
+ Flowboard React Native SDK for SDUI onboarding flows.
4
4
 
5
- ## Installation
5
+ It supports:
6
+
7
+ - Expo development builds and prebuild/EAS workflows
8
+ - React Native CLI apps
9
+ - JSON-driven onboarding with custom screens and full Flowboard renderer support
10
+
11
+ ## Quick Start
12
+
13
+ Install the SDK:
6
14
 
7
15
  ```sh
8
16
  npm install flowboard-react
9
17
  ```
10
18
 
11
- Install compatible versions of the peer/native dependencies for your app that satisfy these minimums:
19
+ Install the native peer dependencies in one step:
12
20
 
13
21
  ```sh
14
- npm install \
15
- @react-native-async-storage/async-storage \
16
- @react-native-masked-view/masked-view \
17
- lottie-react-native \
18
- react-native-device-info \
19
- react-native-get-random-values \
20
- react-native-in-app-review \
21
- react-native-linear-gradient \
22
- react-native-mask-input \
23
- react-native-pager-view \
24
- react-native-permissions \
25
- react-native-safe-area-context \
26
- react-native-svg \
27
- react-native-vector-icons
22
+ npx --package flowboard-react flowboard-setup
28
23
  ```
29
24
 
30
- `flowboard-react` ships `expr-eval` and `uuid` itself, so host apps do not need to install them separately.
31
-
32
- iOS:
25
+ Preview the commands first:
33
26
 
34
27
  ```sh
35
- cd ios && pod install
28
+ npx --package flowboard-react flowboard-setup --dry-run
29
+ ```
30
+
31
+ `flowboard-react` keeps heavy native modules as peer dependencies to avoid duplicate binaries and version conflicts in host apps. The setup command installs the required peer set for you.
32
+
33
+ ## Expo Support
34
+
35
+ Expo Go is not supported. This SDK depends on custom native modules, so Expo users need a development build, `expo prebuild`, or EAS Build.
36
+
37
+ After running `flowboard-setup`, add the plugin to your app config:
38
+
39
+ ```json
40
+ {
41
+ "expo": {
42
+ "plugins": ["flowboard-react"]
43
+ }
44
+ }
36
45
  ```
37
46
 
47
+ If your onboarding flow uses `request_permission` actions, configure the plugin with permission options:
48
+
49
+ ```json
50
+ {
51
+ "expo": {
52
+ "plugins": [
53
+ [
54
+ "flowboard-react",
55
+ {
56
+ "permissions": ["camera", "photos", "microphone"],
57
+ "permissionMessages": {
58
+ "camera": "Allow $(PRODUCT_NAME) to access the camera during onboarding.",
59
+ "photos": "Allow $(PRODUCT_NAME) to access your photo library during onboarding.",
60
+ "microphone": "Allow $(PRODUCT_NAME) to access the microphone during onboarding."
61
+ }
62
+ }
63
+ ]
64
+ ]
65
+ }
66
+ }
67
+ ```
68
+
69
+ Supported plugin options:
70
+
71
+ - `permissions`: `notifications`, `contacts`, `location`, `camera`, `microphone`, `photos`
72
+ - `permissionMessages`: override default iOS usage descriptions for supported Flowboard permissions
73
+ - `iosPermissions`: append advanced `react-native-permissions` iOS entries directly
74
+ - `androidPermissions`: append raw Android permissions directly
75
+ - `includeVectorIconFonts`: defaults to `true`
76
+ - `iconFonts`: override the bundled FontAwesome6 font file list
77
+
78
+ The plugin does three things:
79
+
80
+ - Composes `react-native-permissions` for iOS permission Podfile setup when needed
81
+ - Adds Android manifest permissions for Flowboard permission actions
82
+ - Bundles FontAwesome 6 fonts through `expo-font` so icons work in Expo builds
83
+
84
+ ## React Native CLI Support
85
+
86
+ `flowboard-setup` installs the validated peer set for React Native CLI apps too, but some native configuration still belongs to the host app.
87
+
88
+ After installation:
89
+
90
+ 1. Run `cd ios && pod install`.
91
+ 2. Configure `react-native-permissions` in your `Podfile`, `Info.plist`, and `AndroidManifest.xml` if your flow uses permission actions.
92
+ 3. Configure `react-native-vector-icons` font bundling if your host app does not already do it.
93
+
94
+ The SDK does not edit your app config files automatically in this phase. It installs the packages and prints the exact next steps.
95
+
96
+ ## What `flowboard-setup` Installs
97
+
98
+ | Package | Why it stays a peer |
99
+ | ------------------------------------------- | ------------------------------ |
100
+ | `@react-native-async-storage/async-storage` | Native persistence backend |
101
+ | `@react-native-masked-view/masked-view` | Native masked rendering |
102
+ | `lottie-react-native` | Native animation runtime |
103
+ | `react-native-device-info` | Native device/app metadata |
104
+ | `react-native-in-app-review` | Native review prompt support |
105
+ | `react-native-linear-gradient` | Native gradient rendering |
106
+ | `react-native-pager-view` | Native paged screen containers |
107
+ | `react-native-permissions` | Native permissions bridge |
108
+ | `react-native-safe-area-context` | Native safe-area insets |
109
+ | `react-native-svg` | Native vector rendering |
110
+ | `react-native-vector-icons` | Native font/icon bundling |
111
+
112
+ Expo apps also get `expo-font` so the plugin can bundle the FontAwesome 6 fonts used by Flowboard icons.
113
+
114
+ These JS-only helpers are bundled by the SDK itself and do not need to be installed by host apps:
115
+
116
+ - `react-native-get-random-values`
117
+ - `react-native-mask-input`
118
+ - `expr-eval`
119
+ - `uuid`
120
+
121
+ ## Installation Flags
122
+
123
+ The setup command supports:
124
+
125
+ - `--dry-run`: print commands without installing anything
126
+ - `--yes`: skip the confirmation prompt
127
+ - `--upgrade`: force reinstall of the validated package set
128
+ - `--verbose`: print each command before it runs
129
+ - `--project-root <path>`: target a different host app directory
130
+
38
131
  ## Usage
39
132
 
133
+ Initialize Flowboard once near app startup:
134
+
40
135
  ```tsx
41
136
  import React from 'react';
42
137
  import { Flowboard, FlowboardProvider } from 'flowboard-react';
@@ -53,13 +148,18 @@ export default function App() {
53
148
  }
54
149
  ```
55
150
 
56
- Launch onboarding:
151
+ Launch the default onboarding:
57
152
 
58
153
  ```tsx
59
154
  await Flowboard.launchOnboarding({
60
155
  customScreenBuilder: (ctx) => {
61
156
  if (ctx.screenData.id === 'paywall') {
62
- return <MyPaywall onClose={ctx.onNext} onSignup={() => ctx.onJumpTo('signup')} />;
157
+ return (
158
+ <MyPaywall
159
+ onClose={ctx.onNext}
160
+ onSignup={() => ctx.onJumpTo('signup')}
161
+ />
162
+ );
63
163
  }
64
164
  return null;
65
165
  },
@@ -69,7 +169,7 @@ await Flowboard.launchOnboarding({
69
169
  });
70
170
  ```
71
171
 
72
- Launch a specific onboarding by ID (without replacing default cached onboarding used by `launchOnboarding`):
172
+ Launch a specific onboarding by ID:
73
173
 
74
174
  ```tsx
75
175
  await Flowboard.launchOnboardingById('YOUR_ONBOARDING_ID', {
@@ -81,7 +181,7 @@ await Flowboard.launchOnboardingById('YOUR_ONBOARDING_ID', {
81
181
  });
82
182
  ```
83
183
 
84
- Re-initialize:
184
+ Re-initialize if you rotate credentials or environment:
85
185
 
86
186
  ```tsx
87
187
  await Flowboard.reinit();
@@ -93,11 +193,11 @@ await Flowboard.reinit();
93
193
  - `Flowboard.launchOnboarding({ customScreenBuilder, customActionBuilder, onOnboardEnd, onStepChange, enableAnalytics, alwaysRestart, resumeProgress })`
94
194
  - `Flowboard.launchOnboardingById(onboardingId, { locale, version, customScreenBuilder, customActionBuilder, onOnboardEnd, onStepChange, enableAnalytics, alwaysRestart, resumeProgress })`
95
195
  - `Flowboard.reinit()`
96
- - `FlowboardProvider` (required to host modal flow)
196
+ - `FlowboardProvider`
97
197
 
98
- ## Jump To Action
198
+ ## Jump To Actions
99
199
 
100
- Configure direct screen navigation in JSON with `action: "jump_to"` and an `actionData.screenId` target:
200
+ Use `jump_to` in Flowboard JSON:
101
201
 
102
202
  ```json
103
203
  {
@@ -117,25 +217,53 @@ Inside custom screens, use `ctx.onJumpTo(screenId)`:
117
217
  ```tsx
118
218
  customScreenBuilder: (ctx) => {
119
219
  if (ctx.screenData.id === 'paywall') {
120
- return <Button title="Jump to Sign Up" onPress={() => ctx.onJumpTo('signup')} />;
220
+ return (
221
+ <Button title="Jump to Sign Up" onPress={() => ctx.onJumpTo('signup')} />
222
+ );
121
223
  }
122
224
  return null;
123
225
  };
124
226
  ```
125
227
 
126
- ## Parity Checklist
228
+ ## Runtime Resilience
229
+
230
+ Flowboard wraps its native peers behind defensive adapters.
231
+
232
+ If a native module is installed but not linked or configured correctly:
233
+
234
+ - the SDK logs a development warning once
235
+ - storage falls back to memory
236
+ - permissions return `unavailable`
237
+ - in-app review is skipped
238
+ - gradients, masks, paging, SVG, and Lottie fall back to simplified rendering where possible
127
239
 
128
- - [x] Flowboard init/launch/reinit
129
- - [x] Client context capture + resolver payload
130
- - [x] Analytics events (onboard_loaded, onboard_started, screen_view, onboard_ended)
131
- - [x] Cache + progress persistence
132
- - [x] SDUI components (layout, visual, inputs, advanced)
133
- - [x] Custom screens/actions
134
- - [x] Deeplink/weblink, rate_app, request_permission, jump_to
135
- - [x] Feature showcase example app
136
- - [x] Jest unit tests for critical logic
240
+ This does not replace installation. Metro still requires the peer packages to exist in the host app dependency graph.
137
241
 
138
- ## Testing
242
+ ## Troubleshooting
243
+
244
+ `Plugin "flowboard-react" was not found`
245
+
246
+ - Make sure `flowboard-react` is installed in the host app.
247
+ - Re-run `npx --package flowboard-react flowboard-setup`.
248
+
249
+ Icons render as empty boxes
250
+
251
+ - Expo: confirm `plugins: ["flowboard-react"]` is present and rebuild the development client.
252
+ - React Native CLI: confirm `react-native-vector-icons` fonts are bundled in the native app.
253
+
254
+ Permission requests do nothing on iOS
255
+
256
+ - Make sure `react-native-permissions` is configured in the host app Podfile.
257
+ - Confirm the matching `Info.plist` usage descriptions exist.
258
+ - Re-run `pod install`.
259
+
260
+ Warnings about unavailable native modules
261
+
262
+ - The package is present, but native setup is incomplete.
263
+ - Rebuild the app after installing peers.
264
+ - Verify `pod install`, Android permissions, and Expo plugin setup for the affected module.
265
+
266
+ ## Development
139
267
 
140
268
  ```sh
141
269
  yarn typecheck
@@ -143,18 +271,6 @@ yarn test
143
271
  yarn lint
144
272
  ```
145
273
 
146
- ## Notes
147
-
148
- - Custom fonts must be available in the host app when using `fontFamily`.
149
- - Asset images/Lottie ("source": "asset") should be provided as `uri` or through a custom loader in the host app.
150
- - Radar charts support optional `autoFitLabels` (default `true`) and `labelOffset` (default `max(20, fontSize * 1.5)`) to keep axis labels inside bounds.
151
-
152
- ## Contributing
153
-
154
- - [Development workflow](CONTRIBUTING.md#development-workflow)
155
- - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
156
- - [Code of conduct](CODE_OF_CONDUCT.md)
157
-
158
274
  ## License
159
275
 
160
276
  MIT
package/app.plugin.js ADDED
@@ -0,0 +1,341 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ const DEFAULT_ICON_FONTS = [
5
+ 'FontAwesome6_Brands.ttf',
6
+ 'FontAwesome6_Regular.ttf',
7
+ 'FontAwesome6_Solid.ttf',
8
+ ];
9
+
10
+ const FLOWBOARD_PERMISSION_MAP = {
11
+ camera: {
12
+ android: [{ name: 'android.permission.CAMERA' }],
13
+ infoPlistKey: 'NSCameraUsageDescription',
14
+ infoPlistMessage:
15
+ 'Allow $(PRODUCT_NAME) to access the camera during onboarding.',
16
+ ios: 'Camera',
17
+ },
18
+ contacts: {
19
+ android: [{ name: 'android.permission.READ_CONTACTS' }],
20
+ infoPlistKey: 'NSContactsUsageDescription',
21
+ infoPlistMessage:
22
+ 'Allow $(PRODUCT_NAME) to access your contacts during onboarding.',
23
+ ios: 'Contacts',
24
+ },
25
+ location: {
26
+ android: [{ name: 'android.permission.ACCESS_FINE_LOCATION' }],
27
+ infoPlistKey: 'NSLocationWhenInUseUsageDescription',
28
+ infoPlistMessage:
29
+ 'Allow $(PRODUCT_NAME) to access your location during onboarding.',
30
+ ios: 'LocationWhenInUse',
31
+ },
32
+ microphone: {
33
+ android: [{ name: 'android.permission.RECORD_AUDIO' }],
34
+ infoPlistKey: 'NSMicrophoneUsageDescription',
35
+ infoPlistMessage:
36
+ 'Allow $(PRODUCT_NAME) to access the microphone during onboarding.',
37
+ ios: 'Microphone',
38
+ },
39
+ notifications: {
40
+ android: [{ name: 'android.permission.POST_NOTIFICATIONS' }],
41
+ ios: 'Notifications',
42
+ },
43
+ photos: {
44
+ android: [
45
+ { name: 'android.permission.READ_MEDIA_IMAGES' },
46
+ {
47
+ attributes: { 'android:maxSdkVersion': '32' },
48
+ name: 'android.permission.READ_EXTERNAL_STORAGE',
49
+ },
50
+ ],
51
+ infoPlistKey: 'NSPhotoLibraryUsageDescription',
52
+ infoPlistMessage:
53
+ 'Allow $(PRODUCT_NAME) to access your photo library during onboarding.',
54
+ ios: 'PhotoLibrary',
55
+ },
56
+ };
57
+
58
+ function unique(values) {
59
+ return [...new Set(values.filter(Boolean))];
60
+ }
61
+
62
+ function getProjectRoot(config) {
63
+ return config?._internal?.projectRoot || process.cwd();
64
+ }
65
+
66
+ function warn(message) {
67
+ console.warn(`[flowboard-react/plugin] ${message}`);
68
+ }
69
+
70
+ function normalizeFlowboardPermissions(permissions) {
71
+ if (!Array.isArray(permissions)) return [];
72
+ return unique(
73
+ permissions
74
+ .map((permission) =>
75
+ String(permission || '')
76
+ .trim()
77
+ .toLowerCase()
78
+ )
79
+ .filter((permission) => FLOWBOARD_PERMISSION_MAP[permission])
80
+ );
81
+ }
82
+
83
+ function resolveIosPermissions(options) {
84
+ const inferred = normalizeFlowboardPermissions(options.permissions).map(
85
+ (permission) => FLOWBOARD_PERMISSION_MAP[permission].ios
86
+ );
87
+ const extras = Array.isArray(options.iosPermissions)
88
+ ? options.iosPermissions.map((permission) =>
89
+ String(permission || '').trim()
90
+ )
91
+ : [];
92
+ return unique([...inferred, ...extras]);
93
+ }
94
+
95
+ function resolveAndroidPermissions(options) {
96
+ const inferred = normalizeFlowboardPermissions(options.permissions).flatMap(
97
+ (permission) => FLOWBOARD_PERMISSION_MAP[permission].android || []
98
+ );
99
+ const extras = Array.isArray(options.androidPermissions)
100
+ ? options.androidPermissions
101
+ .map((permission) => String(permission || '').trim())
102
+ .filter(Boolean)
103
+ .map((name) => ({ name }))
104
+ : [];
105
+
106
+ return [...inferred, ...extras].reduce((acc, entry) => {
107
+ const name = entry.name;
108
+ const attributes = entry.attributes || {};
109
+ const duplicate = acc.some(
110
+ (candidate) =>
111
+ candidate.name === name &&
112
+ JSON.stringify(candidate.attributes || {}) ===
113
+ JSON.stringify(attributes)
114
+ );
115
+ if (!duplicate) {
116
+ acc.push({ attributes, name });
117
+ }
118
+ return acc;
119
+ }, []);
120
+ }
121
+
122
+ function resolveInfoPlistEntries(options) {
123
+ const overrides = options.permissionMessages || {};
124
+ return normalizeFlowboardPermissions(options.permissions).reduce(
125
+ (acc, permission) => {
126
+ const config = FLOWBOARD_PERMISSION_MAP[permission];
127
+ if (!config.infoPlistKey) return acc;
128
+ acc[config.infoPlistKey] =
129
+ overrides[permission] || config.infoPlistMessage;
130
+ return acc;
131
+ },
132
+ {}
133
+ );
134
+ }
135
+
136
+ function resolveOptionalModule(projectRoot, requests) {
137
+ const candidates = Array.isArray(requests) ? requests : [requests];
138
+ for (const request of candidates) {
139
+ try {
140
+ const resolvedPath = require.resolve(request, { paths: [projectRoot] });
141
+ return {
142
+ module: require(resolvedPath),
143
+ resolvedPath,
144
+ };
145
+ } catch {
146
+ continue;
147
+ }
148
+ }
149
+ return null;
150
+ }
151
+
152
+ function createEnvironment(projectRoot) {
153
+ const configPlugins = resolveOptionalModule(
154
+ projectRoot,
155
+ '@expo/config-plugins'
156
+ );
157
+ return {
158
+ configPlugins: configPlugins ? configPlugins.module : null,
159
+ loadOptionalModule: (requests) =>
160
+ resolveOptionalModule(projectRoot, requests),
161
+ projectRoot,
162
+ warn,
163
+ };
164
+ }
165
+
166
+ function applyPermissionsPlugin(config, options, env) {
167
+ const iosPermissions = resolveIosPermissions(options);
168
+ if (iosPermissions.length === 0) return config;
169
+
170
+ const pluginModule = env.loadOptionalModule([
171
+ 'react-native-permissions/app.plugin.js',
172
+ 'react-native-permissions',
173
+ ]);
174
+ if (!pluginModule) {
175
+ env.warn(
176
+ 'react-native-permissions was not found in the host app. iOS Podfile setup was skipped.'
177
+ );
178
+ return config;
179
+ }
180
+
181
+ const plugin = pluginModule.module.default || pluginModule.module;
182
+ if (typeof plugin !== 'function') {
183
+ env.warn(
184
+ 'react-native-permissions did not expose a valid Expo plugin. iOS Podfile setup was skipped.'
185
+ );
186
+ return config;
187
+ }
188
+
189
+ return plugin(config, { iosPermissions });
190
+ }
191
+
192
+ function resolveVectorIconFontPaths(options, env) {
193
+ if (options.includeVectorIconFonts === false) return [];
194
+
195
+ const iconFonts =
196
+ Array.isArray(options.iconFonts) && options.iconFonts.length > 0
197
+ ? options.iconFonts
198
+ : DEFAULT_ICON_FONTS;
199
+
200
+ const packageJson = env.loadOptionalModule(
201
+ 'react-native-vector-icons/package.json'
202
+ );
203
+ if (!packageJson) {
204
+ env.warn(
205
+ 'react-native-vector-icons was not found in the host app. Font bundling for Expo was skipped.'
206
+ );
207
+ return [];
208
+ }
209
+
210
+ const packageRoot = path.dirname(packageJson.resolvedPath);
211
+ return iconFonts
212
+ .map((fontFile) => path.join(packageRoot, 'Fonts', fontFile))
213
+ .filter((fontPath) => {
214
+ if (fs.existsSync(fontPath)) return true;
215
+ env.warn(`Missing vector icon font file: ${fontPath}`);
216
+ return false;
217
+ });
218
+ }
219
+
220
+ function applyExpoFontPlugin(config, options, env) {
221
+ const fonts = resolveVectorIconFontPaths(options, env);
222
+ if (fonts.length === 0) return config;
223
+
224
+ const pluginModule = env.loadOptionalModule([
225
+ 'expo-font/app.plugin.js',
226
+ 'expo-font',
227
+ ]);
228
+ if (!pluginModule) {
229
+ env.warn(
230
+ 'expo-font was not found in the host app. Font bundling for Expo was skipped.'
231
+ );
232
+ return config;
233
+ }
234
+
235
+ const plugin = pluginModule.module.default || pluginModule.module;
236
+ if (typeof plugin !== 'function') {
237
+ env.warn(
238
+ 'expo-font did not expose a valid Expo plugin. Font bundling was skipped.'
239
+ );
240
+ return config;
241
+ }
242
+
243
+ return plugin(config, { fonts });
244
+ }
245
+
246
+ function ensureAndroidPermission(manifest, permission) {
247
+ const usesPermissions = manifest.manifest['uses-permission'] || [];
248
+ const nextEntry = {
249
+ $: {
250
+ 'android:name': permission.name,
251
+ ...(permission.attributes || {}),
252
+ },
253
+ };
254
+
255
+ const duplicate = usesPermissions.some((entry) => {
256
+ const current = entry.$ || {};
257
+ return (
258
+ current['android:name'] === nextEntry.$['android:name'] &&
259
+ current['android:maxSdkVersion'] === nextEntry.$['android:maxSdkVersion']
260
+ );
261
+ });
262
+
263
+ if (!duplicate) {
264
+ usesPermissions.push(nextEntry);
265
+ }
266
+
267
+ manifest.manifest['uses-permission'] = usesPermissions;
268
+ return manifest;
269
+ }
270
+
271
+ function applyFlowboardPlugin(config, options = {}, env) {
272
+ const configPlugins = env.configPlugins;
273
+ if (
274
+ !configPlugins ||
275
+ typeof configPlugins.withAndroidManifest !== 'function' ||
276
+ typeof configPlugins.withInfoPlist !== 'function'
277
+ ) {
278
+ env.warn(
279
+ '@expo/config-plugins was not found in the host app. Flowboard Expo configuration was skipped.'
280
+ );
281
+ return config;
282
+ }
283
+
284
+ let nextConfig = config;
285
+ nextConfig = applyPermissionsPlugin(nextConfig, options, env);
286
+ nextConfig = applyExpoFontPlugin(nextConfig, options, env);
287
+
288
+ nextConfig = configPlugins.withInfoPlist(nextConfig, (infoConfig) => {
289
+ const entries = resolveInfoPlistEntries(options);
290
+ infoConfig.modResults = infoConfig.modResults || {};
291
+
292
+ Object.entries(entries).forEach(([key, value]) => {
293
+ if (!infoConfig.modResults[key]) {
294
+ infoConfig.modResults[key] = value;
295
+ }
296
+ });
297
+
298
+ return infoConfig;
299
+ });
300
+
301
+ nextConfig = configPlugins.withAndroidManifest(
302
+ nextConfig,
303
+ (androidConfig) => {
304
+ androidConfig.modResults = androidConfig.modResults || { manifest: {} };
305
+ androidConfig.modResults.manifest =
306
+ androidConfig.modResults.manifest || {};
307
+
308
+ resolveAndroidPermissions(options).forEach((permission) => {
309
+ ensureAndroidPermission(androidConfig.modResults, permission);
310
+ });
311
+
312
+ return androidConfig;
313
+ }
314
+ );
315
+
316
+ return nextConfig;
317
+ }
318
+
319
+ function withFlowboardReact(config, options = {}) {
320
+ const projectRoot = getProjectRoot(config);
321
+ const env = createEnvironment(projectRoot);
322
+ return applyFlowboardPlugin(config, options, env);
323
+ }
324
+
325
+ module.exports = withFlowboardReact;
326
+ module.exports._internal = {
327
+ DEFAULT_ICON_FONTS,
328
+ FLOWBOARD_PERMISSION_MAP,
329
+ applyExpoFontPlugin,
330
+ applyFlowboardPlugin,
331
+ applyPermissionsPlugin,
332
+ createEnvironment,
333
+ ensureAndroidPermission,
334
+ getProjectRoot,
335
+ normalizeFlowboardPermissions,
336
+ resolveAndroidPermissions,
337
+ resolveInfoPlistEntries,
338
+ resolveIosPermissions,
339
+ resolveOptionalModule,
340
+ resolveVectorIconFontPaths,
341
+ };