@userflow-tech/react-native 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +160 -126
- package/docs/anchoring.md +3 -4
- package/docs/troubleshooting.md +27 -24
- package/expo-plugin/hostDependencyWarnings.d.ts +5 -2
- package/expo-plugin/hostDependencyWarnings.d.ts.map +1 -1
- package/expo-plugin/hostDependencyWarnings.js +12 -2
- package/expo-plugin/hostDependencyWarnings.js.map +1 -1
- package/lib/commonjs/authoring/capturePickerScreenshot.js +9 -1
- package/lib/commonjs/authoring/capturePickerScreenshot.js.map +1 -1
- package/lib/commonjs/evalJs/getWebView.js +5 -13
- package/lib/commonjs/evalJs/getWebView.js.map +1 -1
- package/lib/commonjs/hostAppVersion.js +6 -1
- package/lib/commonjs/hostAppVersion.js.map +1 -1
- package/lib/commonjs/installCheck.js +61 -29
- package/lib/commonjs/installCheck.js.map +1 -1
- package/lib/commonjs/nativeModules.js +44 -0
- package/lib/commonjs/nativeModules.js.map +1 -0
- package/lib/commonjs/network.js +20 -7
- package/lib/commonjs/network.js.map +1 -1
- package/lib/commonjs/pairing/tokenStorage.js +17 -4
- package/lib/commonjs/pairing/tokenStorage.js.map +1 -1
- package/lib/commonjs/surfaces/BannerSurface.js +3 -1
- package/lib/commonjs/surfaces/BannerSurface.js.map +1 -1
- package/lib/commonjs/surfaces/safeArea.js +6 -0
- package/lib/commonjs/surfaces/safeArea.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/module/authoring/capturePickerScreenshot.js +9 -1
- package/lib/module/authoring/capturePickerScreenshot.js.map +1 -1
- package/lib/module/evalJs/getWebView.js +5 -13
- package/lib/module/evalJs/getWebView.js.map +1 -1
- package/lib/module/hostAppVersion.js +6 -1
- package/lib/module/hostAppVersion.js.map +1 -1
- package/lib/module/installCheck.js +56 -26
- package/lib/module/installCheck.js.map +1 -1
- package/lib/module/nativeModules.js +40 -0
- package/lib/module/nativeModules.js.map +1 -0
- package/lib/module/network.js +20 -7
- package/lib/module/network.js.map +1 -1
- package/lib/module/pairing/tokenStorage.js +17 -4
- package/lib/module/pairing/tokenStorage.js.map +1 -1
- package/lib/module/surfaces/BannerSurface.js +3 -1
- package/lib/module/surfaces/BannerSurface.js.map +1 -1
- package/lib/module/surfaces/safeArea.js +5 -0
- package/lib/module/surfaces/safeArea.js.map +1 -1
- package/lib/module/version.js +1 -1
- package/lib/typescript/commonjs/src/authoring/capturePickerScreenshot.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/evalJs/getWebView.d.ts +2 -1
- package/lib/typescript/commonjs/src/evalJs/getWebView.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/hostAppVersion.d.ts +1 -1
- package/lib/typescript/commonjs/src/hostAppVersion.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/installCheck.d.ts +18 -3
- package/lib/typescript/commonjs/src/installCheck.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/nativeModules.d.ts +13 -0
- package/lib/typescript/commonjs/src/nativeModules.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/network.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/pairing/tokenStorage.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/surfaces/BannerSurface.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/surfaces/safeArea.d.ts +2 -0
- package/lib/typescript/commonjs/src/surfaces/safeArea.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/version.d.ts +1 -1
- package/lib/typescript/module/src/authoring/capturePickerScreenshot.d.ts.map +1 -1
- package/lib/typescript/module/src/evalJs/getWebView.d.ts +2 -1
- package/lib/typescript/module/src/evalJs/getWebView.d.ts.map +1 -1
- package/lib/typescript/module/src/hostAppVersion.d.ts +1 -1
- package/lib/typescript/module/src/hostAppVersion.d.ts.map +1 -1
- package/lib/typescript/module/src/installCheck.d.ts +18 -3
- package/lib/typescript/module/src/installCheck.d.ts.map +1 -1
- package/lib/typescript/module/src/nativeModules.d.ts +13 -0
- package/lib/typescript/module/src/nativeModules.d.ts.map +1 -0
- package/lib/typescript/module/src/network.d.ts.map +1 -1
- package/lib/typescript/module/src/pairing/tokenStorage.d.ts.map +1 -1
- package/lib/typescript/module/src/surfaces/BannerSurface.d.ts.map +1 -1
- package/lib/typescript/module/src/surfaces/safeArea.d.ts +2 -0
- package/lib/typescript/module/src/surfaces/safeArea.d.ts.map +1 -1
- package/lib/typescript/module/src/version.d.ts +1 -1
- package/package.json +13 -8
- package/src/authoring/capturePickerScreenshot.ts +10 -1
- package/src/evalJs/getWebView.ts +6 -13
- package/src/hostAppVersion.ts +7 -1
- package/src/installCheck.ts +67 -33
- package/src/nativeModules.ts +48 -0
- package/src/network.ts +27 -11
- package/src/pairing/tokenStorage.ts +23 -7
- package/src/surfaces/BannerSurface.tsx +7 -5
- package/src/surfaces/safeArea.ts +5 -0
- package/src/version.ts +1 -1
- package/vendor/core/package.json +1 -1
- package/vendor/protocol/package.json +1 -1
- package/docs/theme-mapping.md +0 -130
- package/docs/webview-custom-banner-poc.md +0 -64
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@userflow-tech/react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Userflow SDK for React Native — onboarding flows, tooltips, and modals.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"main": "./lib/commonjs/index.js",
|
|
@@ -86,15 +86,16 @@
|
|
|
86
86
|
"clean": "rimraf lib vendor expo-plugin babel-plugin"
|
|
87
87
|
},
|
|
88
88
|
"peerDependencies": {
|
|
89
|
-
"@react-native-community/netinfo": ">=11.0.0",
|
|
89
|
+
"@react-native-community/netinfo": ">=11.0.0 <13.0.0",
|
|
90
90
|
"@react-navigation/native": ">=6.0.0",
|
|
91
91
|
"expo-router": ">=3.0.0",
|
|
92
92
|
"expo-secure-store": ">=13.0.0",
|
|
93
93
|
"react": ">=18.2.0",
|
|
94
|
-
"react-native": ">=0.
|
|
95
|
-
"react-native-
|
|
96
|
-
"react-native-
|
|
97
|
-
"react-native-
|
|
94
|
+
"react-native": ">=0.76.0",
|
|
95
|
+
"react-native-device-info": ">=11.0.0 <16.0.0",
|
|
96
|
+
"react-native-safe-area-context": ">=4.0.0 <6.0.0",
|
|
97
|
+
"react-native-view-shot": "^5.0.0",
|
|
98
|
+
"react-native-webview": ">=13.8.0 <14.0.0"
|
|
98
99
|
},
|
|
99
100
|
"peerDependenciesMeta": {
|
|
100
101
|
"@react-native-community/netinfo": {
|
|
@@ -109,12 +110,15 @@
|
|
|
109
110
|
"expo-secure-store": {
|
|
110
111
|
"optional": true
|
|
111
112
|
},
|
|
113
|
+
"react-native-device-info": {
|
|
114
|
+
"optional": true
|
|
115
|
+
},
|
|
112
116
|
"react-native-safe-area-context": {
|
|
113
117
|
"optional": true
|
|
114
118
|
}
|
|
115
119
|
},
|
|
116
120
|
"dependencies": {
|
|
117
|
-
"@react-native-async-storage/async-storage": ">=1.23.0",
|
|
121
|
+
"@react-native-async-storage/async-storage": ">=1.23.0 <3.0.0",
|
|
118
122
|
"@userflow-tech/core": "file:./vendor/core",
|
|
119
123
|
"@userflow-tech/protocol": "file:./vendor/protocol"
|
|
120
124
|
},
|
|
@@ -134,6 +138,7 @@
|
|
|
134
138
|
"react": "19.1.0",
|
|
135
139
|
"react-native": "0.81.5",
|
|
136
140
|
"react-native-builder-bob": "^0.30.0",
|
|
141
|
+
"react-native-device-info": "15.0.2",
|
|
137
142
|
"react-native-safe-area-context": "~5.6.0",
|
|
138
143
|
"react-native-view-shot": "^5.1.1",
|
|
139
144
|
"react-native-webview": "13.16.1",
|
|
@@ -166,6 +171,6 @@
|
|
|
166
171
|
]
|
|
167
172
|
},
|
|
168
173
|
"publishConfig": {
|
|
169
|
-
"access": "
|
|
174
|
+
"access": "public"
|
|
170
175
|
}
|
|
171
176
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { Dimensions } from 'react-native';
|
|
2
2
|
|
|
3
|
+
import { isNativeModuleLinked } from '../nativeModules';
|
|
4
|
+
|
|
3
5
|
import type { ElementPickerScreenshot } from './elementPickerController';
|
|
4
6
|
import type { HitRect } from './hitTest';
|
|
5
7
|
|
|
6
8
|
const MAX_CAPTURE_WIDTH = 480;
|
|
9
|
+
const VIEW_SHOT_MODULE = 'RNViewShot';
|
|
7
10
|
|
|
8
11
|
const MISSING_VIEW_SHOT_WARNING =
|
|
9
12
|
'[Userflow] Required peer react-native-view-shot is not installed; ' +
|
|
@@ -31,8 +34,14 @@ function warnMissingViewShot(): void {
|
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
function loadViewShot(): ViewShotModule | null {
|
|
37
|
+
// view-shot 4 calls TurboModuleRegistry.getEnforcing at import. Metro reports
|
|
38
|
+
// that throw as fatal before this try/catch can see it.
|
|
39
|
+
if (!isNativeModuleLinked(VIEW_SHOT_MODULE)) {
|
|
40
|
+
warnMissingViewShot();
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
34
43
|
try {
|
|
35
|
-
// Required peer; degrade if
|
|
44
|
+
// Required peer; degrade if the JS package is missing.
|
|
36
45
|
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
|
|
37
46
|
return require('react-native-view-shot') as ViewShotModule;
|
|
38
47
|
} catch {
|
package/src/evalJs/getWebView.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ComponentType, Ref } from 'react';
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import { isNativeModuleLinked } from '../nativeModules';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Minimal surface of `react-native-webview` this spike uses.
|
|
@@ -45,20 +46,11 @@ let warnedMissing = false;
|
|
|
45
46
|
/**
|
|
46
47
|
* True when the native `react-native-webview` binary is linked. Do not
|
|
47
48
|
* `require('react-native-webview')` before this passes — the JS entry calls
|
|
48
|
-
* `TurboModuleRegistry.getEnforcing('RNCWebViewModule')` and
|
|
49
|
+
* `TurboModuleRegistry.getEnforcing('RNCWebViewModule')` and Metro reports that
|
|
50
|
+
* throw as fatal before try/catch.
|
|
49
51
|
*/
|
|
50
52
|
export function isEvalJsWebViewNativeLinked(): boolean {
|
|
51
|
-
|
|
52
|
-
if (typeof TurboModuleRegistry?.get === 'function') {
|
|
53
|
-
if (TurboModuleRegistry.get('RNCWebViewModule') != null) {
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
const legacy = NativeModules.RNCWebViewModule as unknown;
|
|
58
|
-
return legacy != null;
|
|
59
|
-
} catch {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
53
|
+
return isNativeModuleLinked('RNCWebViewModule');
|
|
62
54
|
}
|
|
63
55
|
|
|
64
56
|
/**
|
|
@@ -101,6 +93,7 @@ export function getEvalJsWebView(): EvalJsWebViewComponent | null {
|
|
|
101
93
|
function warnMissing(reason: 'native' | 'js' | 'unavailable'): void {
|
|
102
94
|
if (warnedMissing) return;
|
|
103
95
|
warnedMissing = true;
|
|
96
|
+
if (!__DEV__) return;
|
|
104
97
|
if (reason === 'native') {
|
|
105
98
|
console.warn(
|
|
106
99
|
'Userflow: react-native-webview is listed in package.json but RNCWebViewModule is not in ' +
|
package/src/hostAppVersion.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Read the host app's native marketing version (store version), not the
|
|
3
3
|
* SDK package version. React Native does not expose the store version on
|
|
4
4
|
* `Platform`; Expo hosts may use optional expo modules; bare RN reads
|
|
5
|
-
* `NativeModules.RNDeviceInfo` constants from the
|
|
5
|
+
* `NativeModules.RNDeviceInfo` constants from the device-info native
|
|
6
6
|
* module (after autolink + rebuild). The OS name is `Platform.OS`.
|
|
7
7
|
*
|
|
8
8
|
* Do not `require('react-native-device-info')` — that package throws at import
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* even inside try/catch.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { isNativeModuleLinked } from './nativeModules';
|
|
14
|
+
|
|
13
15
|
export type HostAppVersionSource =
|
|
14
16
|
| 'expo-application'
|
|
15
17
|
| 'expo-constants'
|
|
@@ -42,6 +44,10 @@ function unwrapModule(mod: unknown): Record<string, unknown> | null {
|
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
function tryRequireExpoApplication(): unknown {
|
|
47
|
+
// expo-application calls requireNativeModule('ExpoApplication') at import.
|
|
48
|
+
// Skip the require when that module is not registered so a missing binary
|
|
49
|
+
// cannot surface as a fatal error.
|
|
50
|
+
if (!isNativeModuleLinked('ExpoApplication')) return undefined;
|
|
45
51
|
try {
|
|
46
52
|
// Optional on bare RN — Metro requires a string literal.
|
|
47
53
|
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
package/src/installCheck.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { isEvalJsWebViewNativeLinked } from './evalJs/getWebView';
|
|
2
|
+
import { isNativeModuleLinked } from './nativeModules';
|
|
2
3
|
import { isAsyncStorageLinked } from './storage/asyncStorage';
|
|
4
|
+
import { isSafeAreaContextInstalled } from './surfaces/safeArea';
|
|
3
5
|
|
|
4
6
|
export const INSTALL_CHECK_HEADER =
|
|
5
7
|
'[Userflow] Native setup is incomplete. Flows may not run until you fix the following, then rebuild the app (a Metro reload is not enough):';
|
|
@@ -16,7 +18,30 @@ export const INSTALL_WEBVIEW_LINE =
|
|
|
16
18
|
export const INSTALL_DEVICE_INFO_LINE =
|
|
17
19
|
'- react-native-device-info is not linked, so app_version and app_build are not sent. npm install react-native-device-info, then rebuild.';
|
|
18
20
|
|
|
19
|
-
export const INSTALL_CHECK_FOOTER =
|
|
21
|
+
export const INSTALL_CHECK_FOOTER =
|
|
22
|
+
'See node_modules/@userflow-tech/react-native/docs/troubleshooting.md.';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Separate from {@link INSTALL_CHECK_HEADER}. Flows still run without
|
|
26
|
+
* safe-area-context; they draw under the status bar.
|
|
27
|
+
*/
|
|
28
|
+
export const INSTALL_SAFE_AREA_WARNING =
|
|
29
|
+
'[Userflow] react-native-safe-area-context is not installed, so banners, flow cards, and Builder bars draw under the status bar and home indicator. Install it (npx expo install react-native-safe-area-context) and wrap the app in <SafeAreaProvider>.\n' +
|
|
30
|
+
INSTALL_CHECK_FOOTER;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Separate from {@link INSTALL_CHECK_HEADER}. Flows still run on view-shot 4;
|
|
34
|
+
* Builder screenshots may not. The installed version is interpolated.
|
|
35
|
+
*/
|
|
36
|
+
export function formatViewShotVersionWarning(version: string): string {
|
|
37
|
+
return (
|
|
38
|
+
`[Userflow] react-native-view-shot@${version} is older than 5.0.0, so Builder element selection may not get a screenshot. ` +
|
|
39
|
+
'Install react-native-view-shot@^5.0.0 in the same command as the SDK and rebuild. ' +
|
|
40
|
+
'Do not retry the install with --force or --legacy-peer-deps; that leaves this version in place. ' +
|
|
41
|
+
'On Expo SDK 54, pin ^5.0.0 and add "react-native-view-shot" to expo.install.exclude.\n' +
|
|
42
|
+
INSTALL_CHECK_FOOTER
|
|
43
|
+
);
|
|
44
|
+
}
|
|
20
45
|
|
|
21
46
|
export const EXPO_GO_WARNING =
|
|
22
47
|
'[Userflow] This app is running in Expo Go, which cannot load the SDK native modules. Builder preview links will not open the app. Use a development build (npx expo run:ios / npx expo run:android).';
|
|
@@ -27,11 +52,6 @@ export const INIT_OPTIONS_IGNORED_WARNING =
|
|
|
27
52
|
const VIEW_SHOT_MODULE = 'RNViewShot';
|
|
28
53
|
const DEVICE_INFO_MODULE = 'RNDeviceInfo';
|
|
29
54
|
|
|
30
|
-
interface NativeLookup {
|
|
31
|
-
NativeModules?: Record<string, unknown>;
|
|
32
|
-
TurboModuleRegistry?: { get?: (name: string) => unknown };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
55
|
interface ExpoConstantsModule {
|
|
36
56
|
executionEnvironment?: string;
|
|
37
57
|
default?: { executionEnvironment?: string };
|
|
@@ -39,31 +59,6 @@ interface ExpoConstantsModule {
|
|
|
39
59
|
|
|
40
60
|
let didWarn = false;
|
|
41
61
|
|
|
42
|
-
function loadReactNative(): NativeLookup | null {
|
|
43
|
-
try {
|
|
44
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
45
|
-
return require('react-native') as NativeLookup;
|
|
46
|
-
} catch {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function isNativeModuleLinked(name: string): boolean {
|
|
52
|
-
const reactNative = loadReactNative();
|
|
53
|
-
if (!reactNative) return false;
|
|
54
|
-
try {
|
|
55
|
-
const turbo = reactNative.TurboModuleRegistry;
|
|
56
|
-
if (typeof turbo?.get === 'function' && turbo.get(name) != null) return true;
|
|
57
|
-
} catch {
|
|
58
|
-
// Fall through to the legacy registry.
|
|
59
|
-
}
|
|
60
|
-
try {
|
|
61
|
-
return reactNative.NativeModules?.[name] != null;
|
|
62
|
-
} catch {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
62
|
function readExpoConstants(): ExpoConstantsModule | null {
|
|
68
63
|
try {
|
|
69
64
|
// Optional. Bare React Native does not have this package.
|
|
@@ -79,6 +74,31 @@ function readExpoConstants(): ExpoConstantsModule | null {
|
|
|
79
74
|
}
|
|
80
75
|
}
|
|
81
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Installed view-shot version, read from package.json only. Requiring the
|
|
79
|
+
* package entry loads the native module, and view-shot 4 calls
|
|
80
|
+
* `TurboModuleRegistry.getEnforcing` at import.
|
|
81
|
+
*/
|
|
82
|
+
function readViewShotVersion(): string | null {
|
|
83
|
+
try {
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
85
|
+
const pkg = require('react-native-view-shot/package.json') as { version?: unknown };
|
|
86
|
+
return typeof pkg.version === 'string' && pkg.version.length > 0 ? pkg.version : null;
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Dev warning when view-shot 4.x is installed. Null on 5.x or when it is absent. */
|
|
93
|
+
export function viewShotVersionWarning(): string | null {
|
|
94
|
+
const version = readViewShotVersion();
|
|
95
|
+
if (!version) return null;
|
|
96
|
+
const match = version.match(/\d+/);
|
|
97
|
+
const major = match ? Number(match[0]) : null;
|
|
98
|
+
if (major == null || major >= 5) return null;
|
|
99
|
+
return formatViewShotVersionWarning(version);
|
|
100
|
+
}
|
|
101
|
+
|
|
82
102
|
/** Lines for missing native modules. Empty when the build looks complete. */
|
|
83
103
|
export function collectInstallFindings(): string[] {
|
|
84
104
|
const findings: string[] = [];
|
|
@@ -104,8 +124,11 @@ export function formatInstallWarning(findings: readonly string[]): string {
|
|
|
104
124
|
}
|
|
105
125
|
|
|
106
126
|
/**
|
|
107
|
-
* One development warning for missing native modules,
|
|
108
|
-
*
|
|
127
|
+
* One development warning for missing native modules, separate warnings when
|
|
128
|
+
* safe-area-context is not installed or view-shot is older than 5, and a
|
|
129
|
+
* separate warning when the host is Expo Go. Release builds stay quiet. The
|
|
130
|
+
* safe-area and old view-shot warnings are not part of the native-module list:
|
|
131
|
+
* flows still run, and that list says they may not.
|
|
109
132
|
*/
|
|
110
133
|
export function warnIfInstallIncomplete(): void {
|
|
111
134
|
if (!__DEV__ || didWarn) return;
|
|
@@ -117,6 +140,17 @@ export function warnIfInstallIncomplete(): void {
|
|
|
117
140
|
console.warn(formatInstallWarning(findings));
|
|
118
141
|
}
|
|
119
142
|
|
|
143
|
+
if (!isSafeAreaContextInstalled()) {
|
|
144
|
+
// eslint-disable-next-line no-console -- intentional install diagnostic
|
|
145
|
+
console.warn(INSTALL_SAFE_AREA_WARNING);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const oldViewShot = viewShotVersionWarning();
|
|
149
|
+
if (oldViewShot) {
|
|
150
|
+
// eslint-disable-next-line no-console -- intentional install diagnostic
|
|
151
|
+
console.warn(oldViewShot);
|
|
152
|
+
}
|
|
153
|
+
|
|
120
154
|
const expoGo = expoGoWarning();
|
|
121
155
|
if (expoGo) {
|
|
122
156
|
// eslint-disable-next-line no-console -- intentional install diagnostic
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a native module is registered in this binary.
|
|
3
|
+
*
|
|
4
|
+
* Check this before `require`ing a package that throws while it loads when the
|
|
5
|
+
* native half is missing. Metro reports that throw as a fatal error before the
|
|
6
|
+
* caller's try/catch runs, which is a red screen in debug and can crash a
|
|
7
|
+
* release build.
|
|
8
|
+
*
|
|
9
|
+
* React Native modules show up on `TurboModuleRegistry` or `NativeModules`.
|
|
10
|
+
* Expo modules are on `globalThis.expo.modules` (see `requireOptionalNativeModule`).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
interface NativeLookup {
|
|
14
|
+
NativeModules?: Record<string, unknown>;
|
|
15
|
+
TurboModuleRegistry?: { get?: (name: string) => unknown };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ExpoRuntime {
|
|
19
|
+
modules?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function loadReactNative(): NativeLookup | null {
|
|
23
|
+
try {
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
25
|
+
return require('react-native') as NativeLookup;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isNativeModuleLinked(name: string): boolean {
|
|
32
|
+
const expoModules = (globalThis as { expo?: ExpoRuntime }).expo?.modules;
|
|
33
|
+
if (expoModules?.[name] != null) return true;
|
|
34
|
+
|
|
35
|
+
const reactNative = loadReactNative();
|
|
36
|
+
if (!reactNative) return false;
|
|
37
|
+
try {
|
|
38
|
+
const turbo = reactNative.TurboModuleRegistry;
|
|
39
|
+
if (typeof turbo?.get === 'function' && turbo.get(name) != null) return true;
|
|
40
|
+
} catch {
|
|
41
|
+
// Fall through to the legacy registry.
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
return reactNative.NativeModules?.[name] != null;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/network.ts
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* recovers within a few seconds of the next beat / foreground return.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { isNativeModuleLinked } from './nativeModules';
|
|
18
|
+
|
|
17
19
|
export interface NetworkStatus {
|
|
18
20
|
isConnected: boolean | null;
|
|
19
21
|
/** `null` means unknown — treat as reachable when connected. */
|
|
@@ -30,10 +32,31 @@ function isOnline(status: NetworkStatus): boolean {
|
|
|
30
32
|
return status.isConnected === true && status.isInternetReachable !== false;
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
const NETINFO_MODULE = 'RNCNetInfo';
|
|
36
|
+
|
|
37
|
+
const NETINFO_UNAVAILABLE_WARNING =
|
|
38
|
+
'[Userflow] Optional peer @react-native-community/netinfo is not installed or not linked; ' +
|
|
39
|
+
'skipping instant network-aware reconnect. Connection recovery still works ' +
|
|
40
|
+
'via the adaptive heartbeat and foreground liveness probe.';
|
|
41
|
+
|
|
42
|
+
function warnNetInfoUnavailable(): void {
|
|
43
|
+
if (!__DEV__) return;
|
|
44
|
+
// eslint-disable-next-line no-console -- intentional missing-optional-peer diagnostic
|
|
45
|
+
console.warn(NETINFO_UNAVAILABLE_WARNING);
|
|
46
|
+
}
|
|
47
|
+
|
|
33
48
|
function defaultSubscribe(handler: (status: NetworkStatus) => void): () => void {
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
49
|
+
// Check the native module before require. NetInfo throws while its module
|
|
50
|
+
// loads when RNCNetInfo is missing, and Metro reports that as fatal before
|
|
51
|
+
// this try/catch can see it.
|
|
52
|
+
if (!isNativeModuleLinked(NETINFO_MODULE)) {
|
|
53
|
+
warnNetInfoUnavailable();
|
|
54
|
+
return () => {};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Lazy require so the SDK works without the optional peer. When installed
|
|
58
|
+
// and linked, NetInfo makes offline→online reconnect fast; when absent the
|
|
59
|
+
// transport's adaptive heartbeat + foreground probe still recover.
|
|
37
60
|
try {
|
|
38
61
|
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires -- optional peer at import time
|
|
39
62
|
const NetInfo = require('@react-native-community/netinfo') as {
|
|
@@ -51,14 +74,7 @@ function defaultSubscribe(handler: (status: NetworkStatus) => void): () => void
|
|
|
51
74
|
});
|
|
52
75
|
});
|
|
53
76
|
} catch {
|
|
54
|
-
|
|
55
|
-
// eslint-disable-next-line no-console -- intentional missing-optional-peer diagnostic
|
|
56
|
-
console.warn(
|
|
57
|
-
'[Userflow] Optional peer @react-native-community/netinfo is not installed; ' +
|
|
58
|
-
'skipping instant network-aware reconnect. Connection recovery still works ' +
|
|
59
|
-
'via the adaptive heartbeat and foreground liveness probe.',
|
|
60
|
-
);
|
|
61
|
-
}
|
|
77
|
+
warnNetInfoUnavailable();
|
|
62
78
|
return () => {};
|
|
63
79
|
}
|
|
64
80
|
}
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import type { PairingPurpose } from '@userflow-tech/core';
|
|
8
8
|
|
|
9
|
+
import { isNativeModuleLinked } from '../nativeModules';
|
|
10
|
+
|
|
9
11
|
import { parsePairingPurpose } from './deepLink';
|
|
10
12
|
|
|
11
13
|
// SecureStore rejects keys outside [A-Za-z0-9._-], so no `userflow:` prefix here.
|
|
@@ -34,12 +36,24 @@ type SecureStoreModule = {
|
|
|
34
36
|
|
|
35
37
|
let cachedSecureStore: SecureStoreModule | null | undefined;
|
|
36
38
|
|
|
39
|
+
const SECURE_STORE_MODULE = 'ExpoSecureStore';
|
|
40
|
+
|
|
41
|
+
const SECURE_STORE_UNAVAILABLE_WARNING =
|
|
42
|
+
'[Userflow] Optional peer expo-secure-store is unavailable; ' +
|
|
43
|
+
'pairing JWT persistence falls back to in-memory storage.';
|
|
44
|
+
|
|
37
45
|
/** Expo modules must be installed natively — not just the JS package. */
|
|
38
46
|
function isExpoModulesRuntimeAvailable(): boolean {
|
|
39
47
|
const expo = (globalThis as { expo?: { EventEmitter?: unknown } }).expo;
|
|
40
48
|
return typeof expo?.EventEmitter === 'function';
|
|
41
49
|
}
|
|
42
50
|
|
|
51
|
+
function warnSecureStoreUnavailable(): void {
|
|
52
|
+
if (!__DEV__) return;
|
|
53
|
+
// eslint-disable-next-line no-console -- intentional missing-optional-peer diagnostic
|
|
54
|
+
console.warn(SECURE_STORE_UNAVAILABLE_WARNING);
|
|
55
|
+
}
|
|
56
|
+
|
|
43
57
|
function tryLoadExpoSecureStore(): SecureStoreModule | null {
|
|
44
58
|
if (cachedSecureStore !== undefined) return cachedSecureStore;
|
|
45
59
|
|
|
@@ -48,19 +62,21 @@ function tryLoadExpoSecureStore(): SecureStoreModule | null {
|
|
|
48
62
|
return cachedSecureStore;
|
|
49
63
|
}
|
|
50
64
|
|
|
65
|
+
// expo-secure-store calls requireNativeModule at import and throws when the
|
|
66
|
+
// native module is missing. Metro reports that as fatal before try/catch.
|
|
67
|
+
if (!isNativeModuleLinked(SECURE_STORE_MODULE)) {
|
|
68
|
+
cachedSecureStore = null;
|
|
69
|
+
warnSecureStoreUnavailable();
|
|
70
|
+
return cachedSecureStore;
|
|
71
|
+
}
|
|
72
|
+
|
|
51
73
|
try {
|
|
52
74
|
// Optional peer — absent in bare RN without expo-secure-store.
|
|
53
75
|
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires -- optional peer at import time
|
|
54
76
|
cachedSecureStore = require('expo-secure-store') as SecureStoreModule;
|
|
55
77
|
} catch {
|
|
56
78
|
cachedSecureStore = null;
|
|
57
|
-
|
|
58
|
-
// eslint-disable-next-line no-console -- intentional missing-optional-peer diagnostic
|
|
59
|
-
console.warn(
|
|
60
|
-
'[Userflow] Optional peer expo-secure-store is unavailable; ' +
|
|
61
|
-
'pairing JWT persistence falls back to in-memory storage.',
|
|
62
|
-
);
|
|
63
|
-
}
|
|
79
|
+
warnSecureStoreUnavailable();
|
|
64
80
|
}
|
|
65
81
|
|
|
66
82
|
return cachedSecureStore;
|
|
@@ -61,11 +61,13 @@ export function BannerSurface({
|
|
|
61
61
|
if (!webView) {
|
|
62
62
|
if (!warnedMissingWebView) {
|
|
63
63
|
warnedMissingWebView = true;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
'
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
if (__DEV__) {
|
|
65
|
+
console.warn(
|
|
66
|
+
'Userflow: WebView banner requires react-native-webview in the native binary. ' +
|
|
67
|
+
'Add it to your app (e.g. npx expo install react-native-webview), run pod install under ios/, ' +
|
|
68
|
+
'then rebuild with expo run:ios (not Expo Go). Falling back to the native banner bar.',
|
|
69
|
+
);
|
|
70
|
+
}
|
|
69
71
|
}
|
|
70
72
|
return <BannerBar session={session} onActions={onActions} onDismiss={onDismiss} />;
|
|
71
73
|
}
|
package/src/surfaces/safeArea.ts
CHANGED
|
@@ -48,6 +48,11 @@ function loadSafeAreaModule(): SafeAreaModule | null {
|
|
|
48
48
|
return cachedModule;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** True when the optional peer's JavaScript package can be loaded. */
|
|
52
|
+
export function isSafeAreaContextInstalled(): boolean {
|
|
53
|
+
return loadSafeAreaModule() != null;
|
|
54
|
+
}
|
|
55
|
+
|
|
51
56
|
function copyInsets(insets: Partial<EdgeInsets> | null | undefined): EdgeInsets | null {
|
|
52
57
|
if (!insets) return null;
|
|
53
58
|
const top = insets.top;
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Generated by scripts/write-version.mjs — do not edit. */
|
|
2
|
-
export const SDK_VERSION = '0.
|
|
2
|
+
export const SDK_VERSION = '0.4.1';
|
package/vendor/core/package.json
CHANGED
package/docs/theme-mapping.md
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
# Theme mapping (web → React Native)
|
|
2
|
-
|
|
3
|
-
Phase 3 maps Userflow web theme tokens onto React Native styles used by flow chrome.
|
|
4
|
-
This is an interim visual baseline aligned with the mobile tooltip/modal design specs.
|
|
5
|
-
|
|
6
|
-
## Card chrome — `resolveBubbleChrome`
|
|
7
|
-
|
|
8
|
-
Source: `src/surfaces/themeChrome.ts`
|
|
9
|
-
|
|
10
|
-
| Web / theme token | RN usage |
|
|
11
|
-
| ----------------------------------------- | ---------------------------------------------------------------- |
|
|
12
|
-
| `backgroundColor` | Card `backgroundColor` |
|
|
13
|
-
| `bubbleBorderRadius` (default `10`) | Card `borderRadius` |
|
|
14
|
-
| `bubbleBorderWidth` / `bubbleBorderColor` | Card border |
|
|
15
|
-
| `tooltipNotchSize` (default `10`) | Tooltip notch size; `0` hides notch |
|
|
16
|
-
| Modal content padding | `24` (mobile; web default is 40) |
|
|
17
|
-
| Tooltip / bubble content padding | `24` (web tooltip) |
|
|
18
|
-
| Shadow | Approximate dual drop-shadow via `shadow*` + Android `elevation` |
|
|
19
|
-
|
|
20
|
-
Modal cards use `overflow: 'hidden'` so the progress bar clips to the radius.
|
|
21
|
-
Tooltip cards use `overflow: 'visible'` so the notch is not clipped.
|
|
22
|
-
|
|
23
|
-
## Progress — `ProgressBar`
|
|
24
|
-
|
|
25
|
-
| Theme token | RN usage |
|
|
26
|
-
| ---------------------------------------- | ------------------------------------------------- |
|
|
27
|
-
| `progressBarEnabled` | Show / hide |
|
|
28
|
-
| `progressBarPosition` (`TOP` / `BOTTOM`) | Placement flush on the card edge |
|
|
29
|
-
| `progressBarType` | `DEFAULT` fraction bar; `NUMBERED` shows `1 of N` |
|
|
30
|
-
| Step `progress` + step index | Fraction via `resolveStepProgress` |
|
|
31
|
-
|
|
32
|
-
## Buttons — `StepButton`
|
|
33
|
-
|
|
34
|
-
| Theme token | RN usage |
|
|
35
|
-
| ------------------------------- | ---------------------------------------------------------------- |
|
|
36
|
-
| `primaryButton*` / widget brand | Primary filled button |
|
|
37
|
-
| Secondary (DEFAULT) | Neutral outline: white fill, `#d1d5db` border, `fontColor` label |
|
|
38
|
-
| Button fragment colors when set | Override defaults per button |
|
|
39
|
-
|
|
40
|
-
## Backdrop & cutout
|
|
41
|
-
|
|
42
|
-
| Theme token | RN usage |
|
|
43
|
-
| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
|
|
44
|
-
| `backdropColor` | Modal full-screen backdrop; tooltip cutout fill |
|
|
45
|
-
| `backdropOpacity` | Parsed float (`parseBackdropOpacity`) |
|
|
46
|
-
| `modalBackdropOnclick` | `DISMISS` → tap backdrop ends flow |
|
|
47
|
-
| Step `backdrop` / `backdropPadding` / `tooltipTargetBlocked` | Tooltip cutout hole, dim, and interaction blocking (bands block by default; hole sealed when `tooltipTargetBlocked`) |
|
|
48
|
-
|
|
49
|
-
## Tooltip target highlight
|
|
50
|
-
|
|
51
|
-
Source: `src/surfaces/resolveHighlightChrome.ts` + `src/surfaces/tooltip/TooltipHighlight.tsx`.
|
|
52
|
-
|
|
53
|
-
Web paints the ring as one CSS `box-shadow` on `.userflowjs-tooltip-backdrop`.
|
|
54
|
-
RN uses a **2px brand stroke + one translucent halo**, plus iOS `shadow*` /
|
|
55
|
-
Android `boxShadow` for blur. Theme spread is a placement gap only — it is
|
|
56
|
-
not painted as a thick solid bar.
|
|
57
|
-
|
|
58
|
-
| Web / theme token | RN usage |
|
|
59
|
-
| -------------------------- | -------------------------------------------------------------------------------- |
|
|
60
|
-
| `backdropHighlightColor` | Stroke color. Near-white (web default `#ffffff`) is treated as unset — see below |
|
|
61
|
-
| `backdropHighlightOpacity` | Caps the halo at `0.32` so it stays a wash |
|
|
62
|
-
| `backdropHighlightRadius` | Halo thickness (default `4`) |
|
|
63
|
-
| `backdropHighlightSpread` | Added to placement pad when type is `OUTSIDE` (default `0`). Not stroke width |
|
|
64
|
-
| `backdropHighlightType` | `OUTSIDE` (default) draws outside the hole; `INSIDE` insets |
|
|
65
|
-
|
|
66
|
-
Placement pad for `OUTSIDE` is `spread + strokeWidth + radius` so the tooltip
|
|
67
|
-
sits clear of the painted ring, not only the theme spread token.
|
|
68
|
-
| Corner radius | `8` + `backdropPadding` (web copies the target's computed radius; RN measure cannot) |
|
|
69
|
-
|
|
70
|
-
Color fallback: non-white `backdropHighlightColor` → `widgetBackgroundColor` →
|
|
71
|
-
`brandBackgroundColor` → `defaultButtonBackgroundColor` → `#1a57e6`. Web's
|
|
72
|
-
`#ffffff` default is skipped so the dimmed cutout does not get a white frame.
|
|
73
|
-
|
|
74
|
-
## Typography
|
|
75
|
-
|
|
76
|
-
| Theme token | RN usage |
|
|
77
|
-
| ------------ | ----------------------------------- |
|
|
78
|
-
| `fontColor` | Body / heading / close affordance |
|
|
79
|
-
| `fontSize` | Base body size |
|
|
80
|
-
| `fontFamily` | Applied where Text styles accept it |
|
|
81
|
-
|
|
82
|
-
## Quote / code-block — TreeDoc chrome
|
|
83
|
-
|
|
84
|
-
Source of truth: web `userflowjs/styles/bubble-frame.styl` in the main Userflow
|
|
85
|
-
web repo (blockquote / `code` / `pre > code`) and TreeDoc markup
|
|
86
|
-
(`blockquote`, `pre > code`). Spacing tokens from web `$padding-*` assume
|
|
87
|
-
**1rem = 16px**.
|
|
88
|
-
|
|
89
|
-
Implementation: `src/content/RichContentRenderer.tsx` + `src/content/themeColor.ts`.
|
|
90
|
-
|
|
91
|
-
| Token | rem | RN px |
|
|
92
|
-
| ------------- | ----- | ----- |
|
|
93
|
-
| `$padding-2` | 0.125 | 2 |
|
|
94
|
-
| `$padding-4` | 0.25 | 4 |
|
|
95
|
-
| `$padding-8` | 0.5 | 8 |
|
|
96
|
-
| `$padding-16` | 1 | 16 |
|
|
97
|
-
|
|
98
|
-
Root content uses `gap: 8` (`ROOT_CONTENT_GAP`). Quote/code add
|
|
99
|
-
`marginBottom: 8` when not last so gap + margin ≈ web’s 16px block spacing.
|
|
100
|
-
|
|
101
|
-
### Quote (`blockquote`)
|
|
102
|
-
|
|
103
|
-
| Web rule | RN |
|
|
104
|
-
| ----------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
105
|
-
| `border-inline-start: 2px solid rgba(fg, 0.15)` | `borderStartWidth: 2`, `borderStartColor` via `foregroundAlpha(fontColor, 0.15)` |
|
|
106
|
-
| `padding: $padding-4 $padding-16` | `paddingVertical: 4`, `paddingHorizontal: 16` |
|
|
107
|
-
| `margin-bottom: $padding-16` when not last | `marginBottom: 8` (+ root gap 8); last painted sibling: 0 |
|
|
108
|
-
| `min-height: var(--userflow-line-height, 24px)` | `minHeight: 24` |
|
|
109
|
-
| Not italic by default | No forced `fontStyle: 'italic'` |
|
|
110
|
-
|
|
111
|
-
### Code-block (`pre > code`) and inline `code` mark
|
|
112
|
-
|
|
113
|
-
| Web rule | RN |
|
|
114
|
-
| -------------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
115
|
-
| Mono stack (`SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace`) | iOS `Menlo`, Android `monospace` |
|
|
116
|
-
| `font-size: calc(font-size * 0.875)` | `fontSize * 0.875` |
|
|
117
|
-
| `background: rgba(fg, 0.05)` | `foregroundAlpha(fontColor, 0.05)` (hex / rgb / rgba) |
|
|
118
|
-
| `border-radius: calc(font-size * 0.25)` | `fontSize * 0.25` |
|
|
119
|
-
| `pre > code` padding `$padding-8` | `padding: 8` + horizontal `ScrollView` (`nestedScrollEnabled`) |
|
|
120
|
-
| Inline `code` padding `$padding-2 $padding-4` | View chrome wrapper + `paddingVertical: 2`, `paddingHorizontal: 4` |
|
|
121
|
-
|
|
122
|
-
## Safe area
|
|
123
|
-
|
|
124
|
-
Surfaces pad with `useSurfaceLayoutInsets`: optional-peer
|
|
125
|
-
`react-native-safe-area-context` (zero fallback) plus a 24px screen margin so
|
|
126
|
-
cards clear Dynamic Island / home indicator regions. While Builder preview is
|
|
127
|
-
active, the measured Preview banner height is added to the top inset so the
|
|
128
|
-
close control is not trapped under the banner. Tall cards cap at that padded
|
|
129
|
-
window height and scroll internally (`overflow: 'hidden'` on the fill; the
|
|
130
|
-
shadow/notch shell stays `overflow: 'visible'`).
|