Skip to content

Commit 0990341

Browse files
feat/support-for-react-native-device-info (#62)
* feat/support-for-react-native-device-info * fix: separated device info function * updated lockfile * requested changes * formatting --------- Co-authored-by: Mateusz Turbański <Mateusz.turbanski@swmansion.com>
1 parent 976bf43 commit 0990341

6 files changed

Lines changed: 166 additions & 19 deletions

File tree

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ npm install @swmansion/react-native-detour
4141
Install required peer dependencies:
4242

4343
```sh
44-
npm install expo-localization expo-clipboard expo-constants expo-device @react-native-async-storage/async-storage expo-application
44+
npm install expo-localization expo-clipboard expo-constants @react-native-async-storage/async-storage expo-application
45+
# Pick ONE of the device-info providers:
46+
npm install expo-device
47+
# – or –
48+
npm install react-native-device-info
4549
```
4650

4751
> You can override the default persistent storage (@react-native-async-storage/async-storage) by providing an alternative storage implementation. Pass your custom storage object via the configuration settings.

packages/react-native-detour/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,18 @@
9393
"expo-device": ">=7.0.0",
9494
"expo-localization": ">=15.0.0",
9595
"react": ">=18",
96-
"react-native": ">=0.72"
96+
"react-native": ">=0.72",
97+
"react-native-device-info": ">=10.0.0"
9798
},
9899
"peerDependenciesMeta": {
99100
"@react-native-async-storage/async-storage": {
100101
"optional": true
102+
},
103+
"expo-device": {
104+
"optional": true
105+
},
106+
"react-native-device-info": {
107+
"optional": true
101108
}
102109
},
103110
"jest": {

packages/react-native-detour/src/links/api/sendUniversalLinkClick.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { Platform } from "react-native";
22

33
import Constants from "expo-constants";
4-
import * as Device from "expo-device";
54

65
import { SDK_HEADER_VALUE } from "../../version";
76
import type { RequiredConfig } from "../types";
7+
import { getSyncDeviceInfo } from "../utils/deviceInfo";
88

99
const API_URL = "https://godetour.dev/api/link/universal-link-click";
1010

@@ -47,11 +47,14 @@ const extractParams = (url: string): Record<string, string> | undefined => {
4747
};
4848

4949
const buildMetadata = (): Record<string, string> => {
50+
const { model, osVersion } = getSyncDeviceInfo();
51+
5052
const raw: Record<string, string | null | undefined> = {
51-
os_version: Device.osVersion,
53+
os_version: osVersion,
5254
app_version: Constants.nativeAppVersion,
53-
device_model: Device.modelName,
55+
device_model: model,
5456
};
57+
5558
return Object.fromEntries(
5659
Object.entries(raw).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
5760
);
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
type ExpoDeviceModule = {
2+
modelName?: string | null;
3+
manufacturer?: string | null;
4+
osVersion?: string | null;
5+
};
6+
7+
type ReactNativeDeviceInfoModule = {
8+
getModel?: () => string;
9+
getSystemVersion?: () => string;
10+
getManufacturerSync?: () => string;
11+
getManufacturer?: () => Promise<string>;
12+
};
13+
14+
type DeviceInfo = {
15+
model: string;
16+
manufacturer: string;
17+
osVersion: string;
18+
};
19+
20+
type SyncDeviceInfo = Pick<DeviceInfo, "model" | "osVersion">;
21+
22+
const UNKNOWN = "unknown";
23+
24+
const normalizeValue = (value: unknown): string => {
25+
if (typeof value === "string" && value.trim().length > 0) {
26+
return value;
27+
}
28+
if (typeof value === "number") {
29+
return String(value);
30+
}
31+
return UNKNOWN;
32+
};
33+
34+
const firstKnown = (...candidates: (() => unknown)[]): string => {
35+
for (const fn of candidates) {
36+
const value = normalizeValue(fn());
37+
if (value !== UNKNOWN) {
38+
return value;
39+
}
40+
}
41+
42+
return UNKNOWN;
43+
};
44+
45+
const firstKnownAsync = async (
46+
...candidates: (() => unknown | Promise<unknown>)[]
47+
): Promise<string> => {
48+
for (const fn of candidates) {
49+
const value = normalizeValue(await fn());
50+
if (value !== UNKNOWN) {
51+
return value;
52+
}
53+
}
54+
55+
return UNKNOWN;
56+
};
57+
58+
// Metro needs string literal in require() during bundle time, with previous version it caused errors
59+
const expoDevice = (() => {
60+
try {
61+
return require("expo-device") as ExpoDeviceModule;
62+
} catch {
63+
return null;
64+
}
65+
})();
66+
67+
const reactNativeDeviceInfo = (() => {
68+
try {
69+
return require("react-native-device-info") as ReactNativeDeviceInfoModule;
70+
} catch {
71+
return null;
72+
}
73+
})();
74+
75+
const getModel = (): string => {
76+
return firstKnown(
77+
() => expoDevice?.modelName,
78+
() => reactNativeDeviceInfo?.getModel?.(),
79+
);
80+
};
81+
82+
const getOsVersion = (): string => {
83+
return firstKnown(
84+
() => expoDevice?.osVersion,
85+
() => reactNativeDeviceInfo?.getSystemVersion?.(),
86+
);
87+
};
88+
89+
const getManufacturer = async (): Promise<string> => {
90+
return firstKnownAsync(
91+
() => expoDevice?.manufacturer,
92+
async () => {
93+
try {
94+
return await reactNativeDeviceInfo?.getManufacturer?.();
95+
} catch {
96+
return UNKNOWN;
97+
}
98+
},
99+
() => reactNativeDeviceInfo?.getManufacturerSync?.(),
100+
);
101+
};
102+
103+
const assertDeviceInfoLibraryAvailable = (): void => {
104+
if (!expoDevice && !reactNativeDeviceInfo) {
105+
throw new Error(
106+
'[react-native-detour] No device info library found. Install either "expo-device" or "react-native-device-info".',
107+
);
108+
}
109+
};
110+
111+
export const getSyncDeviceInfo = (): SyncDeviceInfo => {
112+
assertDeviceInfoLibraryAvailable();
113+
114+
return {
115+
model: getModel(),
116+
osVersion: getOsVersion(),
117+
};
118+
};
119+
120+
export const getDeviceInfo = async (): Promise<DeviceInfo> => {
121+
assertDeviceInfoLibraryAvailable();
122+
123+
const manufacturer = await getManufacturer();
124+
125+
return {
126+
model: getModel(),
127+
manufacturer,
128+
osVersion: getOsVersion(),
129+
};
130+
};

packages/react-native-detour/src/links/utils/fingerprint.ts

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import { Dimensions, PixelRatio, Platform } from "react-native";
22

33
import * as Clipboard from "expo-clipboard";
44
import Constants from "expo-constants";
5-
import * as Device from "expo-device";
65
import * as Localization from "expo-localization";
76

7+
import { getDeviceInfo } from "./deviceInfo";
8+
89
export type ProbabilisticFingerprint = {
910
platform: string;
1011
model: string;
@@ -40,19 +41,7 @@ export const getProbabilisticFingerprint = async (
4041
languageTag: locale.languageTag,
4142
}));
4243

43-
const normalizeValue = (value: unknown): string => {
44-
if (typeof value === "string" && value.trim().length > 0) {
45-
return value;
46-
}
47-
if (typeof value === "number") {
48-
return String(value);
49-
}
50-
return "unknown";
51-
};
52-
53-
const model = normalizeValue(Device.modelName);
54-
const manufacturer = normalizeValue(Device.manufacturer);
55-
const systemVersion = normalizeValue(Device.osVersion);
44+
const { model, manufacturer, osVersion: systemVersion } = await getDeviceInfo();
5645

5746
let userAgent = "unknown";
5847
if (typeof Constants.getWebViewUserAgentAsync === "function") {

pnpm-lock.yaml

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)