@userflow-tech/react-native 0.3.0 → 0.3.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 +325 -63
- package/docs/anchoring.md +58 -42
- package/docs/troubleshooting.md +63 -27
- package/expo-plugin/hostDependencyWarnings.d.ts +18 -0
- package/expo-plugin/hostDependencyWarnings.d.ts.map +1 -0
- package/expo-plugin/hostDependencyWarnings.js +91 -0
- package/expo-plugin/hostDependencyWarnings.js.map +1 -0
- package/expo-plugin/withUserflow.d.ts.map +1 -1
- package/expo-plugin/withUserflow.js +16 -0
- package/expo-plugin/withUserflow.js.map +1 -1
- package/lib/commonjs/anchors/AnchorRegistry.js +7 -0
- package/lib/commonjs/anchors/AnchorRegistry.js.map +1 -1
- package/lib/commonjs/anchors/elementConditionEvaluator.js +189 -55
- package/lib/commonjs/anchors/elementConditionEvaluator.js.map +1 -1
- package/lib/commonjs/anchors/elementTapMonitor.js +92 -0
- package/lib/commonjs/anchors/elementTapMonitor.js.map +1 -0
- package/lib/commonjs/anchors/elementTargetMatch.js +85 -0
- package/lib/commonjs/anchors/elementTargetMatch.js.map +1 -0
- package/lib/commonjs/anchors/fiber/findAnchorFiber.js +64 -39
- package/lib/commonjs/anchors/fiber/findAnchorFiber.js.map +1 -1
- package/lib/commonjs/anchors/fiber/touchTargetFiber.js +105 -0
- package/lib/commonjs/anchors/fiber/touchTargetFiber.js.map +1 -0
- package/lib/commonjs/anchors/resolveAnchorHost.js +2 -1
- package/lib/commonjs/anchors/resolveAnchorHost.js.map +1 -1
- package/lib/commonjs/anchors/resolveAutoAnchorHost.js +4 -2
- package/lib/commonjs/anchors/resolveAutoAnchorHost.js.map +1 -1
- package/lib/commonjs/anchors/resolveElementTargets.js +158 -0
- package/lib/commonjs/anchors/resolveElementTargets.js.map +1 -0
- package/lib/commonjs/anchors/targetPressMonitor.js +9 -3
- package/lib/commonjs/anchors/targetPressMonitor.js.map +1 -1
- package/lib/commonjs/anchors/useUserflowAnchor.js +1 -0
- package/lib/commonjs/anchors/useUserflowAnchor.js.map +1 -1
- package/lib/commonjs/anonymousId.js +16 -7
- package/lib/commonjs/anonymousId.js.map +1 -1
- package/lib/commonjs/index.js +5 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/installCheck.js +151 -0
- package/lib/commonjs/installCheck.js.map +1 -0
- package/lib/commonjs/pairing/deviceId.js +14 -11
- package/lib/commonjs/pairing/deviceId.js.map +1 -1
- package/lib/commonjs/runtime/TargetPressBridge.js +12 -1
- package/lib/commonjs/runtime/TargetPressBridge.js.map +1 -1
- package/lib/commonjs/runtime/UserflowProvider.js +6 -3
- package/lib/commonjs/runtime/UserflowProvider.js.map +1 -1
- package/lib/commonjs/storage/asyncStorage.js +67 -0
- package/lib/commonjs/storage/asyncStorage.js.map +1 -0
- package/lib/commonjs/surfaces/safeArea.js +68 -28
- package/lib/commonjs/surfaces/safeArea.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/module/anchors/AnchorRegistry.js +7 -0
- package/lib/module/anchors/AnchorRegistry.js.map +1 -1
- package/lib/module/anchors/elementConditionEvaluator.js +189 -55
- package/lib/module/anchors/elementConditionEvaluator.js.map +1 -1
- package/lib/module/anchors/elementTapMonitor.js +87 -0
- package/lib/module/anchors/elementTapMonitor.js.map +1 -0
- package/lib/module/anchors/elementTargetMatch.js +78 -0
- package/lib/module/anchors/elementTargetMatch.js.map +1 -0
- package/lib/module/anchors/fiber/findAnchorFiber.js +62 -39
- package/lib/module/anchors/fiber/findAnchorFiber.js.map +1 -1
- package/lib/module/anchors/fiber/touchTargetFiber.js +100 -0
- package/lib/module/anchors/fiber/touchTargetFiber.js.map +1 -0
- package/lib/module/anchors/resolveAnchorHost.js +2 -1
- package/lib/module/anchors/resolveAnchorHost.js.map +1 -1
- package/lib/module/anchors/resolveAutoAnchorHost.js +3 -3
- package/lib/module/anchors/resolveAutoAnchorHost.js.map +1 -1
- package/lib/module/anchors/resolveElementTargets.js +152 -0
- package/lib/module/anchors/resolveElementTargets.js.map +1 -0
- package/lib/module/anchors/targetPressMonitor.js +8 -3
- package/lib/module/anchors/targetPressMonitor.js.map +1 -1
- package/lib/module/anchors/useUserflowAnchor.js +1 -0
- package/lib/module/anchors/useUserflowAnchor.js.map +1 -1
- package/lib/module/anonymousId.js +15 -5
- package/lib/module/anonymousId.js.map +1 -1
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/installCheck.js +139 -0
- package/lib/module/installCheck.js.map +1 -0
- package/lib/module/pairing/deviceId.js +14 -10
- package/lib/module/pairing/deviceId.js.map +1 -1
- package/lib/module/runtime/TargetPressBridge.js +12 -1
- package/lib/module/runtime/TargetPressBridge.js.map +1 -1
- package/lib/module/runtime/UserflowProvider.js +7 -4
- package/lib/module/runtime/UserflowProvider.js.map +1 -1
- package/lib/module/storage/asyncStorage.js +61 -0
- package/lib/module/storage/asyncStorage.js.map +1 -0
- package/lib/module/surfaces/safeArea.js +65 -28
- package/lib/module/surfaces/safeArea.js.map +1 -1
- package/lib/module/version.js +1 -1
- package/lib/typescript/commonjs/src/anchors/AnchorRegistry.d.ts +7 -0
- package/lib/typescript/commonjs/src/anchors/AnchorRegistry.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/anchors/elementConditionEvaluator.d.ts +12 -16
- package/lib/typescript/commonjs/src/anchors/elementConditionEvaluator.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/anchors/elementTapMonitor.d.ts +36 -0
- package/lib/typescript/commonjs/src/anchors/elementTapMonitor.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/anchors/elementTargetMatch.d.ts +20 -0
- package/lib/typescript/commonjs/src/anchors/elementTargetMatch.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/anchors/fiber/findAnchorFiber.d.ts +17 -5
- package/lib/typescript/commonjs/src/anchors/fiber/findAnchorFiber.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/anchors/fiber/touchTargetFiber.d.ts +24 -0
- package/lib/typescript/commonjs/src/anchors/fiber/touchTargetFiber.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/anchors/resolveAnchorHost.d.ts +2 -1
- package/lib/typescript/commonjs/src/anchors/resolveAnchorHost.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/anchors/resolveAutoAnchorHost.d.ts +10 -1
- package/lib/typescript/commonjs/src/anchors/resolveAutoAnchorHost.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/anchors/resolveElementTargets.d.ts +39 -0
- package/lib/typescript/commonjs/src/anchors/resolveElementTargets.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/anchors/targetPressMonitor.d.ts +2 -0
- package/lib/typescript/commonjs/src/anchors/targetPressMonitor.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/anchors/useUserflowAnchor.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/anonymousId.d.ts +2 -0
- package/lib/typescript/commonjs/src/anonymousId.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/installCheck.d.ts +33 -0
- package/lib/typescript/commonjs/src/installCheck.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/pairing/deviceId.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/runtime/TargetPressBridge.d.ts +2 -1
- package/lib/typescript/commonjs/src/runtime/TargetPressBridge.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/runtime/UserflowProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/storage/asyncStorage.d.ts +15 -0
- package/lib/typescript/commonjs/src/storage/asyncStorage.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/surfaces/safeArea.d.ts +4 -2
- 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/anchors/AnchorRegistry.d.ts +7 -0
- package/lib/typescript/module/src/anchors/AnchorRegistry.d.ts.map +1 -1
- package/lib/typescript/module/src/anchors/elementConditionEvaluator.d.ts +12 -16
- package/lib/typescript/module/src/anchors/elementConditionEvaluator.d.ts.map +1 -1
- package/lib/typescript/module/src/anchors/elementTapMonitor.d.ts +36 -0
- package/lib/typescript/module/src/anchors/elementTapMonitor.d.ts.map +1 -0
- package/lib/typescript/module/src/anchors/elementTargetMatch.d.ts +20 -0
- package/lib/typescript/module/src/anchors/elementTargetMatch.d.ts.map +1 -0
- package/lib/typescript/module/src/anchors/fiber/findAnchorFiber.d.ts +17 -5
- package/lib/typescript/module/src/anchors/fiber/findAnchorFiber.d.ts.map +1 -1
- package/lib/typescript/module/src/anchors/fiber/touchTargetFiber.d.ts +24 -0
- package/lib/typescript/module/src/anchors/fiber/touchTargetFiber.d.ts.map +1 -0
- package/lib/typescript/module/src/anchors/resolveAnchorHost.d.ts +2 -1
- package/lib/typescript/module/src/anchors/resolveAnchorHost.d.ts.map +1 -1
- package/lib/typescript/module/src/anchors/resolveAutoAnchorHost.d.ts +10 -1
- package/lib/typescript/module/src/anchors/resolveAutoAnchorHost.d.ts.map +1 -1
- package/lib/typescript/module/src/anchors/resolveElementTargets.d.ts +39 -0
- package/lib/typescript/module/src/anchors/resolveElementTargets.d.ts.map +1 -0
- package/lib/typescript/module/src/anchors/targetPressMonitor.d.ts +2 -0
- package/lib/typescript/module/src/anchors/targetPressMonitor.d.ts.map +1 -1
- package/lib/typescript/module/src/anchors/useUserflowAnchor.d.ts.map +1 -1
- package/lib/typescript/module/src/anonymousId.d.ts +2 -0
- package/lib/typescript/module/src/anonymousId.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/installCheck.d.ts +33 -0
- package/lib/typescript/module/src/installCheck.d.ts.map +1 -0
- package/lib/typescript/module/src/pairing/deviceId.d.ts.map +1 -1
- package/lib/typescript/module/src/runtime/TargetPressBridge.d.ts +2 -1
- package/lib/typescript/module/src/runtime/TargetPressBridge.d.ts.map +1 -1
- package/lib/typescript/module/src/runtime/UserflowProvider.d.ts.map +1 -1
- package/lib/typescript/module/src/storage/asyncStorage.d.ts +15 -0
- package/lib/typescript/module/src/storage/asyncStorage.d.ts.map +1 -0
- package/lib/typescript/module/src/surfaces/safeArea.d.ts +4 -2
- 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 -1
- package/src/anchors/AnchorRegistry.ts +12 -0
- package/src/anchors/elementConditionEvaluator.ts +235 -70
- package/src/anchors/elementTapMonitor.ts +95 -0
- package/src/anchors/elementTargetMatch.ts +90 -0
- package/src/anchors/fiber/findAnchorFiber.ts +66 -35
- package/src/anchors/fiber/touchTargetFiber.ts +125 -0
- package/src/anchors/resolveAnchorHost.ts +2 -1
- package/src/anchors/resolveAutoAnchorHost.ts +3 -3
- package/src/anchors/resolveElementTargets.ts +165 -0
- package/src/anchors/targetPressMonitor.ts +12 -3
- package/src/anchors/useUserflowAnchor.ts +1 -0
- package/src/anonymousId.ts +19 -8
- package/src/index.ts +10 -0
- package/src/installCheck.ts +191 -0
- package/src/pairing/deviceId.ts +14 -11
- package/src/runtime/TargetPressBridge.tsx +17 -1
- package/src/runtime/UserflowProvider.tsx +11 -4
- package/src/storage/asyncStorage.ts +88 -0
- package/src/surfaces/safeArea.ts +80 -40
- package/src/version.ts +1 -1
- package/vendor/core/dist/constants.d.ts +2 -0
- package/vendor/core/dist/constants.d.ts.map +1 -1
- package/vendor/core/dist/constants.js +2 -0
- package/vendor/core/dist/constants.js.map +1 -1
- package/vendor/core/dist/engine/ClientConditionEngine.d.ts +20 -1
- package/vendor/core/dist/engine/ClientConditionEngine.d.ts.map +1 -1
- package/vendor/core/dist/engine/ClientConditionEngine.js +13 -10
- package/vendor/core/dist/engine/ClientConditionEngine.js.map +1 -1
- package/vendor/core/dist/engine/UserflowCore.d.ts.map +1 -1
- package/vendor/core/dist/engine/UserflowCore.js +21 -2
- package/vendor/core/dist/engine/UserflowCore.js.map +1 -1
- package/vendor/core/dist/index.d.ts +1 -1
- package/vendor/core/dist/index.d.ts.map +1 -1
- package/vendor/core/dist/index.js.map +1 -1
- package/vendor/core/dist/version.d.ts +1 -1
- package/vendor/core/dist/version.js +1 -1
- package/vendor/core/package.json +1 -1
- package/vendor/protocol/package.json +1 -1
|
@@ -71,6 +71,7 @@ export function useUserflowAnchor(id: string): React.RefCallback<View> {
|
|
|
71
71
|
measureInWindow: (callback) => {
|
|
72
72
|
node.measureInWindow(callback);
|
|
73
73
|
},
|
|
74
|
+
node,
|
|
74
75
|
scrollIntoView: () => {
|
|
75
76
|
const scrollable = node as View & {
|
|
76
77
|
scrollIntoView?: (options?: { block?: string; inline?: string }) => void;
|
package/src/anonymousId.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
1
|
import { UserflowAuthError } from '@userflow-tech/core';
|
|
3
2
|
|
|
3
|
+
import { getAsyncStorage } from './storage/asyncStorage';
|
|
4
|
+
|
|
4
5
|
/** Matches the web SDK's `userflow:anonymousId` localStorage key. */
|
|
5
6
|
export const ANONYMOUS_ID_STORAGE_KEY = 'userflow:anonymousId';
|
|
6
7
|
|
|
8
|
+
export const ANONYMOUS_STORAGE_UNAVAILABLE =
|
|
9
|
+
'Userflow.identifyAnonymous() is not supported when persistent storage is unavailable.';
|
|
10
|
+
|
|
11
|
+
export const ANONYMOUS_STORAGE_NOT_LINKED =
|
|
12
|
+
'Userflow.identifyAnonymous() needs @react-native-async-storage/async-storage, which is not ' +
|
|
13
|
+
"linked in this native build. Add it to your app's package.json, run pod install, and rebuild.";
|
|
14
|
+
|
|
7
15
|
export function uuid(): string {
|
|
8
16
|
const webcrypto = globalThis.crypto;
|
|
9
17
|
if (webcrypto?.randomUUID) return webcrypto.randomUUID();
|
|
@@ -15,24 +23,27 @@ export function uuid(): string {
|
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
export async function getOrCreateAnonymousId(): Promise<string> {
|
|
26
|
+
const storage = getAsyncStorage();
|
|
27
|
+
if (!storage) {
|
|
28
|
+
throw new UserflowAuthError(ANONYMOUS_STORAGE_NOT_LINKED, { isHostContract: true });
|
|
29
|
+
}
|
|
18
30
|
try {
|
|
19
|
-
let externalId = await
|
|
31
|
+
let externalId = await storage.getItem(ANONYMOUS_ID_STORAGE_KEY);
|
|
20
32
|
if (!externalId) {
|
|
21
33
|
externalId = `anon-${uuid()}`;
|
|
22
|
-
await
|
|
34
|
+
await storage.setItem(ANONYMOUS_ID_STORAGE_KEY, externalId);
|
|
23
35
|
}
|
|
24
36
|
return externalId;
|
|
25
37
|
} catch {
|
|
26
|
-
throw new UserflowAuthError(
|
|
27
|
-
'Userflow.identifyAnonymous() is not supported when persistent storage is unavailable.',
|
|
28
|
-
{ isHostContract: true },
|
|
29
|
-
);
|
|
38
|
+
throw new UserflowAuthError(ANONYMOUS_STORAGE_UNAVAILABLE, { isHostContract: true });
|
|
30
39
|
}
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
export async function clearAnonymousId(): Promise<void> {
|
|
43
|
+
const storage = getAsyncStorage();
|
|
44
|
+
if (!storage) return;
|
|
34
45
|
try {
|
|
35
|
-
await
|
|
46
|
+
await storage.removeItem(ANONYMOUS_ID_STORAGE_KEY);
|
|
36
47
|
} catch {
|
|
37
48
|
// Best-effort clear, mirroring the web SDK when storage access is denied.
|
|
38
49
|
}
|
package/src/index.ts
CHANGED
|
@@ -52,6 +52,12 @@ import {
|
|
|
52
52
|
mergeHostAppVersionUserAttributes,
|
|
53
53
|
type HostAppVersion,
|
|
54
54
|
} from './hostAppVersion';
|
|
55
|
+
import {
|
|
56
|
+
commitInitOptions,
|
|
57
|
+
releaseFailedInitOptions,
|
|
58
|
+
warnIfInitOptionsIgnored,
|
|
59
|
+
warnIfInstallIncomplete,
|
|
60
|
+
} from './installCheck';
|
|
55
61
|
import { LifecycleController } from './lifecycle';
|
|
56
62
|
import {
|
|
57
63
|
openFlowNavigateUrl,
|
|
@@ -285,10 +291,12 @@ async function afterIdentify(): Promise<void> {
|
|
|
285
291
|
export const Userflow = {
|
|
286
292
|
/** Validate credentials, store configuration, and wire app-lifecycle handling. */
|
|
287
293
|
init(config: UserflowInitConfig): Promise<UserflowConfig> {
|
|
294
|
+
warnIfInitOptionsIgnored(config);
|
|
288
295
|
cacheHostAppVersionAtInit({
|
|
289
296
|
...(config.appVersion !== undefined ? { appVersion: config.appVersion } : {}),
|
|
290
297
|
...(config.appBuild !== undefined ? { appBuild: config.appBuild } : {}),
|
|
291
298
|
});
|
|
299
|
+
warnIfInstallIncomplete();
|
|
292
300
|
cacheDeviceContextAtInit();
|
|
293
301
|
setHostOnNavigate(config.onNavigate);
|
|
294
302
|
lifecycle.start();
|
|
@@ -318,6 +326,7 @@ export const Userflow = {
|
|
|
318
326
|
|
|
319
327
|
return core.init(config).then(
|
|
320
328
|
(resolved) => {
|
|
329
|
+
commitInitOptions();
|
|
321
330
|
enableScreenTracking();
|
|
322
331
|
// Pairing connect can take the full socket timeout. Identify, and the
|
|
323
332
|
// end-user socket that delivers the preview, must not wait on it.
|
|
@@ -326,6 +335,7 @@ export const Userflow = {
|
|
|
326
335
|
return resolved;
|
|
327
336
|
},
|
|
328
337
|
(error: unknown) => {
|
|
338
|
+
releaseFailedInitOptions();
|
|
329
339
|
pairing.notifyInitFailed(error);
|
|
330
340
|
throw error;
|
|
331
341
|
},
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { isEvalJsWebViewNativeLinked } from './evalJs/getWebView';
|
|
2
|
+
import { isAsyncStorageLinked } from './storage/asyncStorage';
|
|
3
|
+
|
|
4
|
+
export const INSTALL_CHECK_HEADER =
|
|
5
|
+
'[Userflow] Native setup is incomplete. Flows may not run until you fix the following, then rebuild the app (a Metro reload is not enough):';
|
|
6
|
+
|
|
7
|
+
export const INSTALL_ASYNC_STORAGE_LINE =
|
|
8
|
+
'- @react-native-async-storage/async-storage is not linked. Anonymous users and Builder pairing will not persist. Add it to the app package.json (npx expo install @react-native-async-storage/async-storage), run pod install on iOS, and rebuild.';
|
|
9
|
+
|
|
10
|
+
export const INSTALL_VIEW_SHOT_LINE =
|
|
11
|
+
'- react-native-view-shot is not linked, so Builder element selection gets no screenshot. Install react-native-view-shot@^5.0.0 and rebuild. On Expo SDK 54, pin ^5.0.0; an unpinned npx expo install resolves 4.0.3.';
|
|
12
|
+
|
|
13
|
+
export const INSTALL_WEBVIEW_LINE =
|
|
14
|
+
'- react-native-webview is not linked. Evaluate JavaScript and WebView banners will not run. Install it (npx expo install react-native-webview) and rebuild.';
|
|
15
|
+
|
|
16
|
+
export const INSTALL_DEVICE_INFO_LINE =
|
|
17
|
+
'- 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
|
+
|
|
19
|
+
export const INSTALL_CHECK_FOOTER = 'See docs/troubleshooting.md.';
|
|
20
|
+
|
|
21
|
+
export const EXPO_GO_WARNING =
|
|
22
|
+
'[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).';
|
|
23
|
+
|
|
24
|
+
export const INIT_OPTIONS_IGNORED_WARNING =
|
|
25
|
+
'[Userflow] Userflow.init() already ran with different options. The new clientToken, serverEndpoint, and pairingEndpoint are ignored because init runs once per process. Userflow.reset() does not apply new credentials. Restart the app to change them.';
|
|
26
|
+
|
|
27
|
+
const VIEW_SHOT_MODULE = 'RNViewShot';
|
|
28
|
+
const DEVICE_INFO_MODULE = 'RNDeviceInfo';
|
|
29
|
+
|
|
30
|
+
interface NativeLookup {
|
|
31
|
+
NativeModules?: Record<string, unknown>;
|
|
32
|
+
TurboModuleRegistry?: { get?: (name: string) => unknown };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ExpoConstantsModule {
|
|
36
|
+
executionEnvironment?: string;
|
|
37
|
+
default?: { executionEnvironment?: string };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let didWarn = false;
|
|
41
|
+
|
|
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
|
+
function readExpoConstants(): ExpoConstantsModule | null {
|
|
68
|
+
try {
|
|
69
|
+
// Optional. Bare React Native does not have this package.
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
71
|
+
const mod = require('expo-constants') as ExpoConstantsModule;
|
|
72
|
+
const record = mod?.default ?? mod;
|
|
73
|
+
if (record == null || typeof record !== 'object') return null;
|
|
74
|
+
// Touch the field so a throwing getter (missing package stand-in) is caught.
|
|
75
|
+
void record.executionEnvironment;
|
|
76
|
+
return record;
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Lines for missing native modules. Empty when the build looks complete. */
|
|
83
|
+
export function collectInstallFindings(): string[] {
|
|
84
|
+
const findings: string[] = [];
|
|
85
|
+
if (!isAsyncStorageLinked()) findings.push(INSTALL_ASYNC_STORAGE_LINE);
|
|
86
|
+
if (!isNativeModuleLinked(VIEW_SHOT_MODULE)) findings.push(INSTALL_VIEW_SHOT_LINE);
|
|
87
|
+
if (!isEvalJsWebViewNativeLinked()) findings.push(INSTALL_WEBVIEW_LINE);
|
|
88
|
+
// Expo reads the store version from expo-constants / expo-application.
|
|
89
|
+
// device-info is the bare React Native path.
|
|
90
|
+
if (readExpoConstants() == null && !isNativeModuleLinked(DEVICE_INFO_MODULE)) {
|
|
91
|
+
findings.push(INSTALL_DEVICE_INFO_LINE);
|
|
92
|
+
}
|
|
93
|
+
return findings;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function expoGoWarning(): string | null {
|
|
97
|
+
const constants = readExpoConstants();
|
|
98
|
+
const environment = constants?.executionEnvironment;
|
|
99
|
+
return environment === 'storeClient' ? EXPO_GO_WARNING : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function formatInstallWarning(findings: readonly string[]): string {
|
|
103
|
+
return [INSTALL_CHECK_HEADER, ...findings, INSTALL_CHECK_FOOTER].join('\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* One development warning for missing native modules, plus a separate warning
|
|
108
|
+
* when the host is Expo Go. Release builds stay quiet.
|
|
109
|
+
*/
|
|
110
|
+
export function warnIfInstallIncomplete(): void {
|
|
111
|
+
if (!__DEV__ || didWarn) return;
|
|
112
|
+
didWarn = true;
|
|
113
|
+
|
|
114
|
+
const findings = collectInstallFindings();
|
|
115
|
+
if (findings.length > 0) {
|
|
116
|
+
// eslint-disable-next-line no-console -- intentional install diagnostic
|
|
117
|
+
console.warn(formatInstallWarning(findings));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const expoGo = expoGoWarning();
|
|
121
|
+
if (expoGo) {
|
|
122
|
+
// eslint-disable-next-line no-console -- intentional install diagnostic
|
|
123
|
+
console.warn(expoGo);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** @internal */
|
|
128
|
+
export function resetInstallCheckForTests(): void {
|
|
129
|
+
didWarn = false;
|
|
130
|
+
recordedInitOptions = null;
|
|
131
|
+
initOptionsCommitted = false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface RecordedInitOptions {
|
|
135
|
+
clientToken: string;
|
|
136
|
+
serverEndpoint: string;
|
|
137
|
+
pairingEndpoint: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let recordedInitOptions: RecordedInitOptions | null = null;
|
|
141
|
+
/** True once an `init` promise has resolved. A rejection before that clears the record. */
|
|
142
|
+
let initOptionsCommitted = false;
|
|
143
|
+
|
|
144
|
+
function recordInitOptions(config: {
|
|
145
|
+
clientToken: string;
|
|
146
|
+
serverEndpoint?: string;
|
|
147
|
+
pairingEndpoint?: string;
|
|
148
|
+
}): RecordedInitOptions {
|
|
149
|
+
return {
|
|
150
|
+
clientToken: config.clientToken,
|
|
151
|
+
serverEndpoint: config.serverEndpoint ?? '',
|
|
152
|
+
pairingEndpoint: config.pairingEndpoint ?? '',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function warnIfInitOptionsIgnored(config: {
|
|
157
|
+
clientToken: string;
|
|
158
|
+
serverEndpoint?: string;
|
|
159
|
+
pairingEndpoint?: string;
|
|
160
|
+
}): void {
|
|
161
|
+
const next = recordInitOptions(config);
|
|
162
|
+
if (!recordedInitOptions) {
|
|
163
|
+
recordedInitOptions = next;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (
|
|
167
|
+
recordedInitOptions.clientToken === next.clientToken &&
|
|
168
|
+
recordedInitOptions.serverEndpoint === next.serverEndpoint &&
|
|
169
|
+
recordedInitOptions.pairingEndpoint === next.pairingEndpoint
|
|
170
|
+
) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (!__DEV__) return;
|
|
174
|
+
// eslint-disable-next-line no-console -- intentional install diagnostic
|
|
175
|
+
console.warn(INIT_OPTIONS_IGNORED_WARNING);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The in-flight `init` resolved, so later calls really are ignored. */
|
|
179
|
+
export function commitInitOptions(): void {
|
|
180
|
+
initOptionsCommitted = true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The in-flight `init` rejected. Core drops its cached promise and will run
|
|
185
|
+
* the next call, so a corrected token or endpoint must not warn.
|
|
186
|
+
* A rejection after a successful init does not forget that success.
|
|
187
|
+
*/
|
|
188
|
+
export function releaseFailedInitOptions(): void {
|
|
189
|
+
if (initOptionsCommitted) return;
|
|
190
|
+
recordedInitOptions = null;
|
|
191
|
+
}
|
package/src/pairing/deviceId.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
-
|
|
3
1
|
import { uuid } from '../anonymousId';
|
|
2
|
+
import { getAsyncStorage } from '../storage/asyncStorage';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Stable id for this install on the pairing channel.
|
|
@@ -16,21 +15,25 @@ let memoized: string | null = null;
|
|
|
16
15
|
export async function getOrCreatePairingDeviceId(): Promise<string> {
|
|
17
16
|
if (memoized !== null) return memoized;
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
const storage = getAsyncStorage();
|
|
19
|
+
if (storage) {
|
|
20
|
+
try {
|
|
21
|
+
const stored = await storage.getItem(PAIRING_DEVICE_ID_STORAGE_KEY);
|
|
22
|
+
if (typeof stored === 'string' && stored.length > 0) {
|
|
23
|
+
memoized = stored;
|
|
24
|
+
return stored;
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
memoized = uuid();
|
|
28
|
+
return memoized;
|
|
24
29
|
}
|
|
25
|
-
} catch {
|
|
26
|
-
memoized = uuid();
|
|
27
|
-
return memoized;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
const created = uuid();
|
|
31
33
|
memoized = created;
|
|
34
|
+
if (!storage) return created;
|
|
32
35
|
try {
|
|
33
|
-
await
|
|
36
|
+
await storage.setItem(PAIRING_DEVICE_ID_STORAGE_KEY, created);
|
|
34
37
|
} catch {
|
|
35
38
|
// Keep the in-memory id for this process. A later launch may mint another.
|
|
36
39
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { StyleSheet, View, type GestureResponderEvent } from 'react-native';
|
|
3
3
|
|
|
4
|
+
import { elementTapMonitor } from '../anchors/elementTapMonitor';
|
|
5
|
+
import { touchBelongsToEvent, touchTargetFiber } from '../anchors/fiber/touchTargetFiber';
|
|
4
6
|
import { targetPressMonitor } from '../anchors/targetPressMonitor';
|
|
5
7
|
|
|
6
8
|
import { changedTouchPoints, forEachChangedTouch } from './changedTouchPoints';
|
|
@@ -17,7 +19,8 @@ export interface TargetPressBridgeProps {
|
|
|
17
19
|
*
|
|
18
20
|
* The Userflow overlay is a sibling above the app, so hole Pressables would
|
|
19
21
|
* intercept presses. Instead we hit-test bubbled `onTouchStart` / `onTouchEnd`
|
|
20
|
-
* against the rect registered by {@link TooltipSurface}
|
|
22
|
+
* against the rect registered by {@link TooltipSurface}, and — while an
|
|
23
|
+
* element click condition is tracked — against the fiber under the finger.
|
|
21
24
|
*
|
|
22
25
|
* Preview-chrome double taps are observed by `DoubleTapBridge` around the
|
|
23
26
|
* whole provider tree — this bridge never sees overlay touches.
|
|
@@ -27,16 +30,27 @@ export interface TargetPressBridgeProps {
|
|
|
27
30
|
export function TargetPressBridge({ children }: TargetPressBridgeProps): React.ReactElement {
|
|
28
31
|
const onTouchStart = React.useCallback((event: GestureResponderEvent) => {
|
|
29
32
|
isolateSync('target-press', () => {
|
|
33
|
+
const observingTaps = elementTapMonitor.subscriberCount > 0;
|
|
30
34
|
forEachChangedTouch(event, (identifier, pageX, pageY) => {
|
|
31
35
|
targetPressMonitor.handleTouchStart(identifier, pageX, pageY);
|
|
36
|
+
if (!observingTaps || !touchBelongsToEvent(event, identifier)) return;
|
|
37
|
+
elementTapMonitor.handleTouchStart(
|
|
38
|
+
identifier,
|
|
39
|
+
pageX,
|
|
40
|
+
pageY,
|
|
41
|
+
touchTargetFiber(event, identifier),
|
|
42
|
+
);
|
|
32
43
|
});
|
|
33
44
|
});
|
|
34
45
|
}, []);
|
|
35
46
|
|
|
36
47
|
const onTouchEnd = React.useCallback((event: GestureResponderEvent) => {
|
|
37
48
|
isolateSync('target-press', () => {
|
|
49
|
+
const observingTaps = elementTapMonitor.subscriberCount > 0;
|
|
38
50
|
forEachChangedTouch(event, (identifier, pageX, pageY) => {
|
|
39
51
|
targetPressMonitor.handleTouchEnd(identifier, pageX, pageY);
|
|
52
|
+
if (!observingTaps) return;
|
|
53
|
+
elementTapMonitor.handleTouchEnd(identifier, pageX, pageY);
|
|
40
54
|
});
|
|
41
55
|
});
|
|
42
56
|
}, []);
|
|
@@ -46,10 +60,12 @@ export function TargetPressBridge({ children }: TargetPressBridgeProps): React.R
|
|
|
46
60
|
const points = changedTouchPoints(event);
|
|
47
61
|
if (points.length === 0) {
|
|
48
62
|
targetPressMonitor.cancelTouch();
|
|
63
|
+
elementTapMonitor.cancelTouch();
|
|
49
64
|
return;
|
|
50
65
|
}
|
|
51
66
|
for (const touch of points) {
|
|
52
67
|
targetPressMonitor.cancelTouch(touch.identifier);
|
|
68
|
+
elementTapMonitor.cancelTouch(touch.identifier);
|
|
53
69
|
}
|
|
54
70
|
});
|
|
55
71
|
}, []);
|
|
@@ -6,7 +6,11 @@ import { FiberScanRoot } from '../anchors/fiber/FiberScanRoot';
|
|
|
6
6
|
import { DoubleTapBridge } from '../authoring/chrome/DoubleTapBridge';
|
|
7
7
|
import { elementPickerController } from '../authoring/elementPickerController';
|
|
8
8
|
import { ABSOLUTE_FILL } from '../surfaces/absoluteFill';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
getSafeAreaInsetsContext,
|
|
11
|
+
useParentSafeAreaInsets,
|
|
12
|
+
useUserflowSafeAreaInsets,
|
|
13
|
+
} from '../surfaces/safeArea';
|
|
10
14
|
import type { EdgeInsets } from '../surfaces/safeArea';
|
|
11
15
|
import { SurfaceLayoutInsetsProvider } from '../surfaces/surfaceLayoutInsets';
|
|
12
16
|
|
|
@@ -198,11 +202,14 @@ function HostSafeArea({
|
|
|
198
202
|
children: React.ReactNode;
|
|
199
203
|
}): React.ReactElement {
|
|
200
204
|
const Context = getSafeAreaInsetsContext();
|
|
201
|
-
|
|
202
|
-
|
|
205
|
+
// Re-provide only when a parent already supplied insets, or when a banner
|
|
206
|
+
// override applies. Supplying a value with no parent would hide the host's
|
|
207
|
+
// own missing SafeAreaProvider from its useSafeAreaInsets() calls.
|
|
208
|
+
const parent = useParentSafeAreaInsets();
|
|
209
|
+
if (!Context || (insets == null && parent == null)) {
|
|
203
210
|
return <>{children}</>;
|
|
204
211
|
}
|
|
205
|
-
return <Context.Provider value={insets ??
|
|
212
|
+
return <Context.Provider value={insets ?? parent}>{children}</Context.Provider>;
|
|
206
213
|
}
|
|
207
214
|
|
|
208
215
|
/** Access the flow session engine from inside {@link UserflowProvider}. */
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
interface ReactNativeNativeLookup {
|
|
2
|
+
NativeModules?: Record<string, unknown>;
|
|
3
|
+
TurboModuleRegistry?: { get?: (name: string) => unknown };
|
|
4
|
+
}
|
|
5
|
+
const ASYNC_STORAGE_NATIVE_MODULES = [
|
|
6
|
+
'RNAsyncStorage',
|
|
7
|
+
'RNCAsyncStorage',
|
|
8
|
+
'RNC_AsyncSQLiteDBStorage',
|
|
9
|
+
'PlatformLocalStorage',
|
|
10
|
+
'AsyncSQLiteDBStorage',
|
|
11
|
+
'AsyncLocalStorage',
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
export interface AsyncStorageLike {
|
|
15
|
+
getItem(key: string): Promise<string | null>;
|
|
16
|
+
setItem(key: string, value: string): Promise<void>;
|
|
17
|
+
removeItem(key: string): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let cached: AsyncStorageLike | null | undefined;
|
|
21
|
+
|
|
22
|
+
function loadReactNative(): ReactNativeNativeLookup | null {
|
|
23
|
+
try {
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
25
|
+
return require('react-native') as ReactNativeNativeLookup;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function nativeModuleRegistered(name: string, reactNative: ReactNativeNativeLookup): boolean {
|
|
32
|
+
try {
|
|
33
|
+
const turbo = reactNative.TurboModuleRegistry;
|
|
34
|
+
if (typeof turbo?.get === 'function' && turbo.get(name) != null) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// A missing turbo registry is not a failed link check.
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
return reactNative.NativeModules?.[name] != null;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** True when some AsyncStorage native binary is in this build. */
|
|
48
|
+
export function isAsyncStorageLinked(): boolean {
|
|
49
|
+
const reactNative = loadReactNative();
|
|
50
|
+
if (!reactNative) return false;
|
|
51
|
+
return ASYNC_STORAGE_NATIVE_MODULES.some((name) => nativeModuleRegistered(name, reactNative));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The AsyncStorage JS API, or `null` when the package or its native module is
|
|
56
|
+
* missing. Never requires the package until the native module is registered.
|
|
57
|
+
*/
|
|
58
|
+
export function getAsyncStorage(): AsyncStorageLike | null {
|
|
59
|
+
if (cached !== undefined) return cached;
|
|
60
|
+
|
|
61
|
+
if (typeof require !== 'function' || !isAsyncStorageLinked()) {
|
|
62
|
+
cached = null;
|
|
63
|
+
return cached;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
68
|
+
const mod = require('@react-native-async-storage/async-storage') as {
|
|
69
|
+
default?: Partial<AsyncStorageLike>;
|
|
70
|
+
} & Partial<AsyncStorageLike>;
|
|
71
|
+
const storage = mod?.default ?? mod;
|
|
72
|
+
cached =
|
|
73
|
+
typeof storage?.getItem === 'function' &&
|
|
74
|
+
typeof storage.setItem === 'function' &&
|
|
75
|
+
typeof storage.removeItem === 'function'
|
|
76
|
+
? (storage as AsyncStorageLike)
|
|
77
|
+
: null;
|
|
78
|
+
} catch {
|
|
79
|
+
cached = null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return cached;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** @internal */
|
|
86
|
+
export function resetAsyncStorageCacheForTests(): void {
|
|
87
|
+
cached = undefined;
|
|
88
|
+
}
|
package/src/surfaces/safeArea.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import React, { type Context } from 'react';
|
|
2
2
|
|
|
3
3
|
export interface EdgeInsets {
|
|
4
4
|
top: number;
|
|
@@ -9,40 +9,97 @@ export interface EdgeInsets {
|
|
|
9
9
|
|
|
10
10
|
const ZERO_INSETS: EdgeInsets = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
11
11
|
|
|
12
|
-
type UseSafeAreaInsetsFn = () => EdgeInsets;
|
|
13
|
-
|
|
14
|
-
let cachedHook: UseSafeAreaInsetsFn | null | undefined;
|
|
15
|
-
let cachedInsetsContext: Context<EdgeInsets | null> | null | undefined;
|
|
16
|
-
|
|
17
12
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
13
|
+
* Used only when `react-native-safe-area-context` is not installed, so
|
|
14
|
+
* {@link useUserflowSafeAreaInsets} can call `useContext` unconditionally.
|
|
20
15
|
*/
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
const FallbackInsetsContext = React.createContext<EdgeInsets | null>(null);
|
|
17
|
+
|
|
18
|
+
export const MISSING_SAFE_AREA_PROVIDER_WARNING =
|
|
19
|
+
'[Userflow] UserflowProvider is not inside a <SafeAreaProvider>, so flow cards use the ' +
|
|
20
|
+
"device's initial safe-area insets. Wrap your app in <SafeAreaProvider> from " +
|
|
21
|
+
'react-native-safe-area-context.';
|
|
22
|
+
|
|
23
|
+
interface SafeAreaModule {
|
|
24
|
+
SafeAreaInsetsContext?: Context<EdgeInsets | null>;
|
|
25
|
+
initialWindowMetrics?: { insets?: Partial<EdgeInsets> } | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let cachedModule: SafeAreaModule | null | undefined;
|
|
29
|
+
let warnedMissingProvider = false;
|
|
30
|
+
|
|
31
|
+
function loadSafeAreaModule(): SafeAreaModule | null {
|
|
32
|
+
if (cachedModule !== undefined) return cachedModule;
|
|
23
33
|
|
|
24
34
|
if (typeof require !== 'function') {
|
|
25
|
-
|
|
26
|
-
return
|
|
35
|
+
cachedModule = null;
|
|
36
|
+
return null;
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
try {
|
|
30
40
|
// Optional peer — Expo / React Navigation apps usually have it.
|
|
31
41
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
32
|
-
const mod = require('react-native-safe-area-context') as
|
|
33
|
-
|
|
34
|
-
};
|
|
35
|
-
cachedHook = typeof mod.useSafeAreaInsets === 'function' ? mod.useSafeAreaInsets : null;
|
|
42
|
+
const mod = require('react-native-safe-area-context') as SafeAreaModule;
|
|
43
|
+
cachedModule = mod?.SafeAreaInsetsContext ? mod : null;
|
|
36
44
|
} catch {
|
|
37
|
-
|
|
45
|
+
cachedModule = null;
|
|
38
46
|
}
|
|
39
47
|
|
|
40
|
-
return
|
|
48
|
+
return cachedModule;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function copyInsets(insets: Partial<EdgeInsets> | null | undefined): EdgeInsets | null {
|
|
52
|
+
if (!insets) return null;
|
|
53
|
+
const top = insets.top;
|
|
54
|
+
const right = insets.right;
|
|
55
|
+
const bottom = insets.bottom;
|
|
56
|
+
const left = insets.left;
|
|
57
|
+
if (
|
|
58
|
+
typeof top !== 'number' ||
|
|
59
|
+
typeof right !== 'number' ||
|
|
60
|
+
typeof bottom !== 'number' ||
|
|
61
|
+
typeof left !== 'number'
|
|
62
|
+
) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return { top, right, bottom, left };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Device insets from the optional peer, before any provider has measured. */
|
|
69
|
+
function readInitialInsets(): EdgeInsets | null {
|
|
70
|
+
return copyInsets(loadSafeAreaModule()?.initialWindowMetrics?.insets);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function warnMissingProvider(): void {
|
|
74
|
+
if (warnedMissingProvider || !__DEV__) return;
|
|
75
|
+
warnedMissingProvider = true;
|
|
76
|
+
// eslint-disable-next-line no-console -- intentional missing-provider diagnostic
|
|
77
|
+
console.warn(MISSING_SAFE_AREA_PROVIDER_WARNING);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Safe-area insets for flow chrome.
|
|
82
|
+
*
|
|
83
|
+
* Reads `SafeAreaInsetsContext` directly. `useSafeAreaInsets()` throws when the
|
|
84
|
+
* peer is installed but no `SafeAreaProvider` is mounted, which would take down
|
|
85
|
+
* the host tree. A missing provider falls back to `initialWindowMetrics`, then
|
|
86
|
+
* zero, and warns once in development.
|
|
87
|
+
*/
|
|
88
|
+
function insetsContext(): Context<EdgeInsets | null> {
|
|
89
|
+
return loadSafeAreaModule()?.SafeAreaInsetsContext ?? FallbackInsetsContext;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Value from the nearest provider, or `null` when the peer or provider is absent. */
|
|
93
|
+
export function useParentSafeAreaInsets(): EdgeInsets | null {
|
|
94
|
+
return copyInsets(React.useContext(insetsContext()));
|
|
41
95
|
}
|
|
42
96
|
|
|
43
|
-
/** Safe-area insets with a zero fallback when the optional peer is missing. */
|
|
44
97
|
export function useUserflowSafeAreaInsets(): EdgeInsets {
|
|
45
|
-
|
|
98
|
+
const fromProvider = useParentSafeAreaInsets();
|
|
99
|
+
if (fromProvider) return fromProvider;
|
|
100
|
+
if (!loadSafeAreaModule()) return ZERO_INSETS;
|
|
101
|
+
warnMissingProvider();
|
|
102
|
+
return readInitialInsets() ?? ZERO_INSETS;
|
|
46
103
|
}
|
|
47
104
|
|
|
48
105
|
/**
|
|
@@ -50,28 +107,11 @@ export function useUserflowSafeAreaInsets(): EdgeInsets {
|
|
|
50
107
|
* In-flow banners override this so host screens do not add the inset the banner already consumed.
|
|
51
108
|
*/
|
|
52
109
|
export function getSafeAreaInsetsContext(): Context<EdgeInsets | null> | null {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (typeof require !== 'function') {
|
|
56
|
-
cachedInsetsContext = null;
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
62
|
-
const mod = require('react-native-safe-area-context') as {
|
|
63
|
-
SafeAreaInsetsContext?: Context<EdgeInsets | null>;
|
|
64
|
-
};
|
|
65
|
-
cachedInsetsContext = mod.SafeAreaInsetsContext ?? null;
|
|
66
|
-
} catch {
|
|
67
|
-
cachedInsetsContext = null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return cachedInsetsContext;
|
|
110
|
+
return loadSafeAreaModule()?.SafeAreaInsetsContext ?? null;
|
|
71
111
|
}
|
|
72
112
|
|
|
73
113
|
/** @internal */
|
|
74
114
|
export function resetSafeAreaInsetsCacheForTests(): void {
|
|
75
|
-
|
|
76
|
-
|
|
115
|
+
cachedModule = undefined;
|
|
116
|
+
warnedMissingProvider = false;
|
|
77
117
|
}
|
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.3.
|
|
2
|
+
export const SDK_VERSION = '0.3.1';
|