@magicpixel/rn-mp-client-sdk 1.13.0 → 1.13.21

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 (104) hide show
  1. package/README.md +163 -14
  2. package/lib/commonjs/common/app-types.js.map +1 -1
  3. package/lib/commonjs/common/constants.js +11 -2
  4. package/lib/commonjs/common/constants.js.map +1 -1
  5. package/lib/commonjs/common/data-store.js +13 -30
  6. package/lib/commonjs/common/data-store.js.map +1 -1
  7. package/lib/commonjs/common/deeplink-helper.js +174 -0
  8. package/lib/commonjs/common/deeplink-helper.js.map +1 -0
  9. package/lib/commonjs/common/device-info-helper.js +168 -0
  10. package/lib/commonjs/common/device-info-helper.js.map +1 -0
  11. package/lib/commonjs/common/event-bus.js +39 -0
  12. package/lib/commonjs/common/event-bus.js.map +1 -1
  13. package/lib/commonjs/common/network-service.js +119 -15
  14. package/lib/commonjs/common/network-service.js.map +1 -1
  15. package/lib/commonjs/common/reporter.js +75 -14
  16. package/lib/commonjs/common/reporter.js.map +1 -1
  17. package/lib/commonjs/common/storage-helper.js +227 -0
  18. package/lib/commonjs/common/storage-helper.js.map +1 -0
  19. package/lib/commonjs/common/utils.js +62 -2
  20. package/lib/commonjs/common/utils.js.map +1 -1
  21. package/lib/commonjs/eedl/eedl.js +198 -44
  22. package/lib/commonjs/eedl/eedl.js.map +1 -1
  23. package/lib/commonjs/index.js +301 -54
  24. package/lib/commonjs/index.js.map +1 -1
  25. package/lib/commonjs/models/mp-client-sdk.js +17 -10
  26. package/lib/commonjs/models/mp-client-sdk.js.map +1 -1
  27. package/lib/commonjs/processors/data-element.processor.js +51 -7
  28. package/lib/commonjs/processors/data-element.processor.js.map +1 -1
  29. package/lib/commonjs/processors/visit-id.processor.js +78 -15
  30. package/lib/commonjs/processors/visit-id.processor.js.map +1 -1
  31. package/lib/module/common/app-types.js.map +1 -1
  32. package/lib/module/common/constants.js +11 -2
  33. package/lib/module/common/constants.js.map +1 -1
  34. package/lib/module/common/data-store.js +13 -30
  35. package/lib/module/common/data-store.js.map +1 -1
  36. package/lib/module/common/deeplink-helper.js +168 -0
  37. package/lib/module/common/deeplink-helper.js.map +1 -0
  38. package/lib/module/common/device-info-helper.js +161 -0
  39. package/lib/module/common/device-info-helper.js.map +1 -0
  40. package/lib/module/common/event-bus.js +39 -0
  41. package/lib/module/common/event-bus.js.map +1 -1
  42. package/lib/module/common/network-service.js +119 -15
  43. package/lib/module/common/network-service.js.map +1 -1
  44. package/lib/module/common/reporter.js +76 -14
  45. package/lib/module/common/reporter.js.map +1 -1
  46. package/lib/module/common/storage-helper.js +221 -0
  47. package/lib/module/common/storage-helper.js.map +1 -0
  48. package/lib/module/common/utils.js +63 -2
  49. package/lib/module/common/utils.js.map +1 -1
  50. package/lib/module/eedl/eedl.js +198 -44
  51. package/lib/module/eedl/eedl.js.map +1 -1
  52. package/lib/module/index.js +290 -53
  53. package/lib/module/index.js.map +1 -1
  54. package/lib/module/models/mp-client-sdk.js +16 -9
  55. package/lib/module/models/mp-client-sdk.js.map +1 -1
  56. package/lib/module/processors/data-element.processor.js +51 -7
  57. package/lib/module/processors/data-element.processor.js.map +1 -1
  58. package/lib/module/processors/visit-id.processor.js +78 -15
  59. package/lib/module/processors/visit-id.processor.js.map +1 -1
  60. package/lib/typescript/{common → src/common}/app-types.d.ts +30 -9
  61. package/lib/typescript/{common → src/common}/constants.d.ts +0 -1
  62. package/lib/typescript/{common → src/common}/data-store.d.ts +3 -8
  63. package/lib/typescript/src/common/deeplink-helper.d.ts +60 -0
  64. package/lib/typescript/src/common/device-info-helper.d.ts +54 -0
  65. package/lib/typescript/src/common/event-bus.d.ts +21 -0
  66. package/lib/typescript/src/common/network-service.d.ts +32 -0
  67. package/lib/typescript/{common → src/common}/reporter.d.ts +2 -1
  68. package/lib/typescript/src/common/storage-helper.d.ts +47 -0
  69. package/lib/typescript/{common → src/common}/utils.d.ts +25 -0
  70. package/lib/typescript/{eedl → src/eedl}/eedl.d.ts +43 -1
  71. package/lib/typescript/{index.d.ts → src/index.d.ts} +39 -5
  72. package/lib/typescript/{models → src/models}/mp-client-sdk.d.ts +7 -0
  73. package/lib/typescript/src/processors/visit-id.processor.d.ts +23 -0
  74. package/package.json +25 -36
  75. package/src/common/app-types.ts +33 -10
  76. package/src/common/constants.ts +0 -6
  77. package/src/common/data-store.ts +8 -30
  78. package/src/common/deeplink-helper.ts +181 -0
  79. package/src/common/device-info-helper.ts +190 -0
  80. package/src/common/event-bus.ts +39 -0
  81. package/src/common/network-service.ts +154 -21
  82. package/src/common/reporter.ts +97 -16
  83. package/src/common/storage-helper.ts +260 -0
  84. package/src/common/utils.ts +63 -2
  85. package/src/eedl/eedl.ts +225 -51
  86. package/src/index.tsx +346 -73
  87. package/src/models/mp-client-sdk.ts +8 -0
  88. package/src/processors/data-element.processor.ts +85 -7
  89. package/src/processors/visit-id.processor.ts +92 -22
  90. package/lib/commonjs/processors/trans-function.processor.js +0 -73
  91. package/lib/commonjs/processors/trans-function.processor.js.map +0 -1
  92. package/lib/module/processors/trans-function.processor.js +0 -66
  93. package/lib/module/processors/trans-function.processor.js.map +0 -1
  94. package/lib/typescript/common/event-bus.d.ts +0 -6
  95. package/lib/typescript/common/network-service.d.ts +0 -8
  96. package/lib/typescript/processors/trans-function.processor.d.ts +0 -12
  97. package/lib/typescript/processors/visit-id.processor.d.ts +0 -9
  98. package/src/processors/trans-function.processor.ts +0 -85
  99. /package/lib/typescript/{common → src/common}/logger.d.ts +0 -0
  100. /package/lib/typescript/{models → src/models}/geo-api-response.d.ts +0 -0
  101. /package/lib/typescript/{processors → src/processors}/data-element.processor.d.ts +0 -0
  102. /package/lib/typescript/{processors → src/processors}/geo-location.processor.d.ts +0 -0
  103. /package/lib/typescript/{processors → src/processors}/qc.processor.d.ts +0 -0
  104. /package/lib/typescript/{processors → src/processors}/tag.processor.d.ts +0 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@magicpixel/rn-mp-client-sdk",
3
- "version": "1.13.0",
4
- "description": "test",
3
+ "version": "1.13.21",
4
+ "description": "React Native SDK for MagicPixel analytics and tag management",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
7
7
  "types": "lib/typescript/index.d.ts",
@@ -10,40 +10,36 @@
10
10
  "files": [
11
11
  "src",
12
12
  "lib",
13
- "android",
14
- "ios",
15
- "cpp",
16
- "rn-mp-client-sdk.podspec",
17
13
  "!lib/typescript/example",
18
- "!android/build",
19
- "!ios/build",
20
14
  "!**/__tests__",
21
15
  "!**/__fixtures__",
22
16
  "!**/__mocks__"
23
17
  ],
24
18
  "scripts": {
25
- "test": "jest",
19
+ "test": "echo 'No tests configured'",
26
20
  "typescript": "tsc --noEmit",
27
21
  "lint": "eslint \"**/*.{js,ts,tsx}\"",
28
22
  "prepare": "bob build",
29
23
  "release": "release-it",
30
24
  "example": "yarn --cwd example",
31
25
  "bootstrap": "yarn example && yarn && yarn example pods",
32
- "postpublish": "node quick-version-increment.js",
33
- "localpublish": "npm publish --@magicpixel:registry=http://localhost:4873 && npm run postpublish"
26
+ "localpublish": "npm publish --@magicpixel:registry=http://localhost:4873"
34
27
  },
35
28
  "keywords": [
36
29
  "react-native",
37
30
  "ios",
38
31
  "android"
39
32
  ],
40
- "repository": "https://github.com/mp",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git@github.com:digital-madtech/mp-rn-client-sdk.git"
36
+ },
41
37
  "author": "MP Engineering <engineering@magicpixel.io> (https://magicpixel.io)",
42
38
  "license": "MIT",
43
39
  "bugs": {
44
- "url": "https://github.com/mp/issues"
40
+ "url": "https://github.com/digital-madtech/mp-rn-client-sdk/issues"
45
41
  },
46
- "homepage": "https://github.com/mp#readme",
42
+ "homepage": "https://github.com/digital-madtech/mp-rn-client-sdk#readme",
47
43
  "publishConfig": {
48
44
  "registry": "https://registry.npmjs.org/",
49
45
  "access": "public"
@@ -53,38 +49,34 @@
53
49
  "@babel/eslint-parser": "7.19.1",
54
50
  "@babel/runtime": "7.26.10",
55
51
  "@commitlint/config-conventional": "17.1.0",
52
+ "@octokit/plugin-paginate-rest": "9.2.2",
53
+ "@octokit/request": "8.4.1",
54
+ "@octokit/request-error": "5.1.1",
56
55
  "@react-native-community/eslint-config": "3.1.0",
57
56
  "@release-it/conventional-changelog": "8.0.1",
58
57
  "@types/flat": "5.0.2",
59
- "@types/jest": "29.0.3",
60
- "@types/react": "~17.0.21",
58
+ "@types/react": "17.0.50",
61
59
  "@types/react-native": "0.72.8",
62
60
  "@types/uuid": "9.0.0",
61
+ "braces": "3.0.3",
63
62
  "commitlint": "17.1.2",
63
+ "cross-spawn": "7.0.5",
64
64
  "eslint": "8.23.1",
65
65
  "eslint-config-prettier": "8.5.0",
66
66
  "eslint-plugin-prettier": "4.2.1",
67
- "jest": "29.1.1",
67
+ "micromatch": "4.0.8",
68
68
  "pod-install": "0.1.38",
69
69
  "prettier": "2.7.1",
70
70
  "react": "18.2.0",
71
71
  "react-native": "0.72.17",
72
72
  "react-native-builder-bob": "0.18.3",
73
73
  "release-it": "17.1.1",
74
- "ts-jest": "29.0.2",
75
74
  "tsconfig-paths": "4.1.0",
76
75
  "typescript": "4.8.3",
77
- "ts-node": "10.9.1",
78
- "@octokit/plugin-paginate-rest": "9.2.2",
79
- "@octokit/request": "8.4.1",
80
- "@octokit/request-error": "5.1.1",
81
- "braces": "3.0.3",
82
- "cross-spawn": "7.0.5",
83
- "micromatch": "4.0.8",
84
76
  "ws": "7.5.10"
85
77
  },
86
78
  "resolutions": {
87
- "@types/react": "17.0.21",
79
+ "@types/react": "17.0.50",
88
80
  "ip": "2.0.2",
89
81
  "pac-resolver": "7.0.1",
90
82
  "socks": "2.7.3",
@@ -93,20 +85,14 @@
93
85
  "socks-proxy-agent": "8.0.2",
94
86
  "agent-base": "7.1.0"
95
87
  },
88
+ "overrides": {
89
+ "diff": "8.0.3",
90
+ "tmp": "0.2.4"
91
+ },
96
92
  "peerDependencies": {
97
93
  "react": "*",
98
94
  "react-native": "*"
99
95
  },
100
- "jest": {
101
- "preset": "react-native",
102
- "transform": {
103
- ".+\\.(t|j)s$": "ts-jest"
104
- },
105
- "modulePathIgnorePatterns": [
106
- "<rootDir>/example/node_modules",
107
- "<rootDir>/lib/"
108
- ]
109
- },
110
96
  "commitlint": {
111
97
  "extends": [
112
98
  "@commitlint/config-conventional"
@@ -174,7 +160,10 @@
174
160
  "flat": "5.0.2",
175
161
  "nanoid": "5.1.5",
176
162
  "performant-array-to-tree": "1.11.0",
163
+ "react-native-device-info": "15.0.1",
164
+ "react-native-get-random-values": "1.11.0",
177
165
  "react-native-url-polyfill": "1.3.0",
166
+ "ulid": "3.0.1",
178
167
  "uuid": "9.0.0"
179
168
  }
180
169
  }
@@ -4,14 +4,6 @@ export interface MapLike<T = any> {
4
4
  [key: string]: T;
5
5
  }
6
6
 
7
- export interface RunQueueModel {
8
- sseOnly: boolean;
9
- name: string;
10
- dcrName: string;
11
- eventId: string;
12
- dcrPayload?: Record<string, any>;
13
- }
14
-
15
7
  export type TypedAny = Record<
16
8
  string,
17
9
  | string
@@ -38,6 +30,7 @@ export interface AppCustomerInfo {
38
30
 
39
31
  export interface AppPageLoad {
40
32
  page_name: string;
33
+ page_url?: string;
41
34
  is_entry?: number;
42
35
  deep_link_url?: string;
43
36
  app_version?: string;
@@ -94,6 +87,7 @@ export interface Report {
94
87
  tagCt: number; // expected tag count on this page
95
88
  dws: 'j' | 'n'; // down stream configuration. java or node
96
89
  v: string; // client sdk version for this call
90
+ devPayload?: string; // base64 encoded developer payload
97
91
  }
98
92
 
99
93
  export interface UrlInfo {
@@ -130,18 +124,47 @@ export interface SdkInitOptions {
130
124
  baseUrl: string;
131
125
  logLevel: string;
132
126
 
127
+ /**
128
+ * @deprecated device_type is now auto-detected and cannot be overridden
129
+ * This field is ignored. The SDK uses Expo/react-native-device-info for reliable detection.
130
+ */
133
131
  device_type?: string;
134
132
 
133
+ /**
134
+ * @deprecated app_version is now auto-detected and cannot be overridden
135
+ * This field is ignored. The SDK will always use the actual app version from the device.
136
+ */
135
137
  app_version?: string;
138
+
139
+ /**
140
+ * @deprecated os_version is now auto-detected and cannot be overridden
141
+ * This field is ignored. The SDK will always use the actual OS version from the device.
142
+ */
136
143
  os_version?: string;
144
+
145
+ /**
146
+ * Event deduplication window in milliseconds
147
+ * If the same event (name + payload) fires within this window, it will be skipped
148
+ * Purchase events are deduplicated by transaction_id regardless of other fields
149
+ * Default: 5000ms (5 seconds)
150
+ * Set to 0 to disable deduplication
151
+ */
152
+ eventDeduplicationWindowMs?: number;
153
+
154
+ /**
155
+ * Callback invoked when SDK initialization fails after all retries
156
+ * Use this to handle initialization failures gracefully in your app
157
+ * @param error The error that caused the failure
158
+ */
159
+ onInitFailure?: (error: Error) => void;
137
160
  }
138
161
 
139
162
  export interface MpDeviceInfo {
140
163
  package_name?: string; // app package name like com.company.starter
141
164
  short_version?: string;
142
165
  long_version?: string;
143
- os_version: string;
144
- device_model_name: string;
166
+ os_version?: string; // Will be auto-detected if not provided
167
+ device_model_name?: string; // Will be auto-detected if not provided
145
168
  locale?: string;
146
169
  timezone_abbreviation?: string;
147
170
  carrier?: string;
@@ -1,14 +1,8 @@
1
- import { customAlphabet } from 'nanoid/non-secure';
2
1
  import type { BaseResourceParamType } from '../models/mp-client-sdk';
3
2
 
4
3
  export class Constants {
5
4
  static PLACEHOLDER_REGEX = new RegExp(/({{)([A-Z_0-9 -]+)(}})/gi);
6
5
 
7
- static LARGE_RANDOM_POOL = customAlphabet(
8
- '1234567090abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
9
- 21
10
- );
11
-
12
6
  static MP_DL_EVT = 'mp_dl_event';
13
7
  static CUST_EVT = 'custom_event';
14
8
 
@@ -4,7 +4,6 @@ import type {
4
4
  MapLike,
5
5
  MpDeviceInfo,
6
6
  QcInfoItem,
7
- RunQueueModel,
8
7
  SdkInitOptions,
9
8
  UrlInfo,
10
9
  VisitInfo,
@@ -21,8 +20,7 @@ import type {
21
20
  } from '../models/mp-client-sdk';
22
21
  import { Platform } from 'react-native';
23
22
  import { Utils } from './utils';
24
-
25
- const CORE_VERSION = '0.8.6';
23
+ import { version as SDK_VERSION } from '../../package.json';
26
24
 
27
25
  export class DataStore {
28
26
  private static isDataStoreInitialized = false;
@@ -34,14 +32,12 @@ export class DataStore {
34
32
  private static clientSdk: MpClientSdk;
35
33
  private static urls: UrlInfo;
36
34
  private static debugId: string;
37
- private static visitorInfo: VisitorInfo;
35
+ private static visitorInfo: VisitorInfo | null;
38
36
  private static visitInfo: VisitInfo;
39
37
  private static pageLang = 'en';
40
38
  private static mpEnvShort: 'stg' | 'prd';
41
39
  private static devicePlatform = 'unknown';
42
40
  private static deviceType = 'mobile';
43
- private static runQueue: RunQueueModel[] = [];
44
- private static isProcessing = false;
45
41
  private static sdkInitOptions: SdkInitOptions;
46
42
  private static deviceInfo: MpDeviceInfo;
47
43
  private static geoInfo: GeoResponse;
@@ -117,16 +113,6 @@ export class DataStore {
117
113
  return true;
118
114
  }
119
115
 
120
- static enQueueTMFire(
121
- sseOnly: boolean,
122
- name: string,
123
- dcrName: string,
124
- eventId: string,
125
- dcrPayload?: Record<string, any>
126
- ): void {
127
- this.runQueue.push({ sseOnly, name, dcrName, eventId, dcrPayload });
128
- }
129
-
130
116
  static isDataStoreReady(): boolean {
131
117
  return this.isDataStoreInitialized;
132
118
  }
@@ -163,18 +149,6 @@ export class DataStore {
163
149
  this.sdkInitOptions = options;
164
150
  }
165
151
 
166
- static deQueueTMFire(): RunQueueModel {
167
- return this.runQueue.length > 0 ? this.runQueue.pop() : null;
168
- }
169
-
170
- static isTagManagerProcessing(): boolean {
171
- return this.isProcessing;
172
- }
173
-
174
- static setTagManagerProcessing(status: boolean): void {
175
- this.isProcessing = status;
176
- }
177
-
178
152
  static getSdkDataElements(): MapLike<ClientSdkDeItem> {
179
153
  return this.clientSdk?.d || {};
180
154
  }
@@ -217,10 +191,14 @@ export class DataStore {
217
191
  : undefined;
218
192
  }
219
193
 
220
- static setVisitorInfo(visitorInfo: VisitorInfo): void {
194
+ static setVisitorInfo(visitorInfo: VisitorInfo | null): void {
221
195
  this.visitorInfo = visitorInfo;
222
196
  }
223
197
 
198
+ static getVisitorInfo(): VisitorInfo | null {
199
+ return this.visitorInfo;
200
+ }
201
+
224
202
  static getDebugId(): string {
225
203
  return this.debugId;
226
204
  }
@@ -380,7 +358,7 @@ export class DataStore {
380
358
  }
381
359
 
382
360
  static getCoreVersion(): string {
383
- return CORE_VERSION;
361
+ return SDK_VERSION;
384
362
  }
385
363
 
386
364
  static getMpEnv(): 'stg' | 'prd' {
@@ -0,0 +1,181 @@
1
+ import { Linking } from 'react-native';
2
+ import { Logger } from './logger';
3
+
4
+ export enum DeepLinkType {
5
+ CUSTOM_SCHEME = 'custom_scheme', // myapp://
6
+ UNIVERSAL_LINK = 'universal_link', // https:// (iOS Universal Links)
7
+ APP_LINK = 'app_link', // https:// (Android App Links)
8
+ HTTP_LINK = 'http_link', // http://
9
+ }
10
+
11
+ /**
12
+ * Helper class to automatically detect and handle deeplinks
13
+ *
14
+ * Supports multiple link types:
15
+ * 1. Custom URL Schemes: myapp://product/123
16
+ * 2. Universal Links (iOS): https://my-site.com/product/123
17
+ * 3. App Links (Android): https://my-site.com/product/123
18
+ * 4. HTTP Links: http://my-site.com/product/123
19
+ *
20
+ * Common use cases:
21
+ * - Ad clicks (Facebook, Google, TikTok ads with http/https URLs)
22
+ * - Push notification deep links
23
+ * - Email marketing links
24
+ * - SMS campaign links
25
+ * - QR codes
26
+ */
27
+ export class DeepLinkHelper {
28
+ private static listener: any = null;
29
+ private static onDeepLinkCallback:
30
+ | ((url: string, linkType: DeepLinkType) => void)
31
+ | null = null;
32
+
33
+ /**
34
+ * Determine the type of deeplink
35
+ * @param url The URL to analyze
36
+ * @returns The type of deeplink
37
+ */
38
+ static getLinkType(url: string): DeepLinkType {
39
+ try {
40
+ if (url.startsWith('https://')) {
41
+ // Universal Links (iOS) and App Links (Android) both use HTTPS
42
+ return DeepLinkType.UNIVERSAL_LINK;
43
+ } else if (url.startsWith('http://')) {
44
+ return DeepLinkType.HTTP_LINK;
45
+ } else {
46
+ // Custom URL scheme (e.g., myapp://)
47
+ return DeepLinkType.CUSTOM_SCHEME;
48
+ }
49
+ } catch (error) {
50
+ Logger.logError('Error determining link type:', error);
51
+ return DeepLinkType.CUSTOM_SCHEME;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Initialize deeplink detection
57
+ * - Checks if app was opened with initial URL (custom scheme, universal link, or HTTP link)
58
+ * - Sets up listener for deeplinks while app is running
59
+ * @param callback Optional callback to be notified when deeplink is detected
60
+ * @returns The initial URL if app was opened with deeplink, null otherwise
61
+ */
62
+ static async initialize(
63
+ callback?: (url: string, linkType: DeepLinkType) => void
64
+ ): Promise<string | null> {
65
+ try {
66
+ // Store callback if provided
67
+ if (callback) {
68
+ this.onDeepLinkCallback = callback;
69
+ }
70
+
71
+ // Check if app was opened with a deeplink
72
+ // This works for:
73
+ // - Custom schemes: myapp://product/123
74
+ // - Universal Links (iOS): https://my-site.com/product/123
75
+ // - App Links (Android): https://my-site.com/product/123
76
+ // - HTTP links: http://my-site.com/product/123
77
+ const initialUrl = await Linking.getInitialURL();
78
+
79
+ if (initialUrl) {
80
+ const linkType = this.getLinkType(initialUrl);
81
+ Logger.logDbg(`App opened with initial ${linkType}:`, initialUrl);
82
+ this.handleDeepLink(initialUrl);
83
+ }
84
+
85
+ // ALWAYS set up listener for deeplinks while app is running
86
+ // This ensures we can detect deeplinks even if app was opened with initial deeplink
87
+ this.setupListener();
88
+
89
+ return initialUrl;
90
+ } catch (error) {
91
+ Logger.logError('Error initializing deeplink detection:', error);
92
+ return null;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Set up listener for deeplinks received while app is running
98
+ * Works for all link types: custom schemes, universal links, app links, HTTP links
99
+ */
100
+ private static setupListener(): void {
101
+ // Remove existing listener if any
102
+ if (this.listener) {
103
+ this.listener.remove();
104
+ }
105
+
106
+ // Listen for deeplinks while app is running (e.g., user clicks link while app is open)
107
+ this.listener = Linking.addEventListener('url', (event) => {
108
+ const linkType = this.getLinkType(event.url);
109
+ Logger.logDbg(`Deeplink received (${linkType}):`, event.url);
110
+ this.handleDeepLink(event.url);
111
+ });
112
+
113
+ Logger.logDbg(
114
+ 'Deeplink listener set up (supports custom schemes, universal/app links, HTTP links)'
115
+ );
116
+ }
117
+
118
+ /**
119
+ * Handle deeplink URL - notify callback if registered
120
+ * @param url The deeplink URL
121
+ */
122
+ private static handleDeepLink(url: string): void {
123
+ if (this.onDeepLinkCallback) {
124
+ const linkType = this.getLinkType(url);
125
+ this.onDeepLinkCallback(url, linkType);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Remove deeplink listener and clear callback
131
+ */
132
+ static cleanup(): void {
133
+ if (this.listener) {
134
+ this.listener.remove();
135
+ this.listener = null;
136
+ }
137
+ this.onDeepLinkCallback = null;
138
+ Logger.logDbg('Deeplink listener and callback cleared');
139
+ }
140
+
141
+ /**
142
+ * Parse UTM parameters and other campaign data from URL
143
+ * @param url The deeplink URL
144
+ * @returns Object containing campaign parameters
145
+ */
146
+ static parseCampaignParams(url: string): Record<string, string> {
147
+ try {
148
+ const urlObj = new URL(url);
149
+ const params: Record<string, string> = {};
150
+
151
+ // Common campaign parameters
152
+ const campaignParams = [
153
+ 'utm_source',
154
+ 'utm_medium',
155
+ 'utm_campaign',
156
+ 'utm_term',
157
+ 'utm_content',
158
+ 'utm_id',
159
+ 'gclid', // Google Click ID
160
+ 'fbclid', // Facebook Click ID
161
+ 'ttclid', // TikTok Click ID
162
+ 'campaign_id',
163
+ 'ad_id',
164
+ 'adset_id',
165
+ 'creative_id',
166
+ ];
167
+
168
+ campaignParams.forEach((param) => {
169
+ const value = urlObj.searchParams.get(param);
170
+ if (value) {
171
+ params[param] = value;
172
+ }
173
+ });
174
+
175
+ return params;
176
+ } catch (error) {
177
+ Logger.logError('Error parsing campaign params from URL:', error);
178
+ return {};
179
+ }
180
+ }
181
+ }
@@ -0,0 +1,190 @@
1
+ import { Platform } from 'react-native';
2
+ import { Logger } from './logger';
3
+
4
+ interface DeviceAppInfo {
5
+ app_version: string;
6
+ build_number: string;
7
+ package_name: string;
8
+ device_model_name: string;
9
+ device_manufacturer: string;
10
+ device_id: string;
11
+ os_name: string;
12
+ os_version: string;
13
+ is_tablet: boolean;
14
+ is_emulator: boolean;
15
+ }
16
+
17
+ /**
18
+ * Helper class to automatically detect device and app information
19
+ * Supports both Expo and bare React Native:
20
+ * 1. Tries Expo modules first (works in Expo Go)
21
+ * 2. Falls back to react-native-device-info (for bare RN)
22
+ * 3. Returns safe defaults if neither is available
23
+ */
24
+ export class DeviceInfoHelper {
25
+ private static expoApplication: any = null;
26
+ private static expoDevice: any = null;
27
+ private static expoConstants: any = null;
28
+ private static deviceInfo: any = null;
29
+ private static initialized = false;
30
+
31
+ /**
32
+ * Try to load Expo modules and react-native-device-info
33
+ * This uses dynamic imports to avoid errors when modules aren't available
34
+ */
35
+ private static async initialize(): Promise<void> {
36
+ if (this.initialized) return;
37
+
38
+ // Try to load Expo modules first (works in Expo Go)
39
+ try {
40
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
41
+ this.expoApplication = require('expo-application');
42
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
43
+ this.expoDevice = require('expo-device');
44
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
45
+ this.expoConstants = require('expo-constants').default;
46
+ Logger.logDbg('Using Expo modules for device info');
47
+ } catch (error) {
48
+ Logger.logDbg(
49
+ 'Expo modules not available, will try react-native-device-info'
50
+ );
51
+ }
52
+
53
+ // Fall back to react-native-device-info (for bare RN)
54
+ if (!this.expoApplication) {
55
+ try {
56
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
57
+ this.deviceInfo = require('react-native-device-info').default;
58
+ Logger.logDbg('Using react-native-device-info for device info');
59
+ } catch (error) {
60
+ Logger.logError(
61
+ 'Neither Expo modules nor react-native-device-info available:',
62
+ error
63
+ );
64
+ }
65
+ }
66
+
67
+ this.initialized = true;
68
+ }
69
+
70
+ /**
71
+ * Get comprehensive app and device information
72
+ * All methods are async-safe and handle errors gracefully
73
+ */
74
+ static async getAppInfo(): Promise<DeviceAppInfo> {
75
+ await this.initialize();
76
+
77
+ try {
78
+ // Try Expo modules first
79
+ if (this.expoApplication && this.expoDevice) {
80
+ return await this.getAppInfoFromExpo();
81
+ }
82
+
83
+ // Fall back to react-native-device-info
84
+ if (this.deviceInfo) {
85
+ return await this.getAppInfoFromDeviceInfo();
86
+ }
87
+
88
+ // Return safe fallback values
89
+ return this.getSafeDefaults();
90
+ } catch (error) {
91
+ Logger.logError('Error getting device info:', error);
92
+ return this.getSafeDefaults();
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Get device info using Expo modules (works in Expo Go)
98
+ */
99
+ private static async getAppInfoFromExpo(): Promise<DeviceAppInfo> {
100
+ const version = this.expoApplication.nativeApplicationVersion || 'unknown';
101
+ const buildNumber = this.expoApplication.nativeBuildVersion || 'unknown';
102
+ const packageName = this.expoApplication.applicationId || 'unknown';
103
+
104
+ return {
105
+ // App information
106
+ app_version: version,
107
+ build_number: buildNumber,
108
+ package_name: packageName,
109
+
110
+ // Device information
111
+ device_model_name: this.expoDevice.modelName || 'unknown',
112
+ device_manufacturer: this.expoDevice.manufacturer || 'unknown',
113
+ device_id: this.expoConstants.sessionId || 'unknown',
114
+
115
+ // OS information
116
+ os_name: Platform.OS,
117
+ os_version:
118
+ this.expoDevice.osVersion || Platform.Version?.toString() || 'unknown',
119
+
120
+ // Additional useful info
121
+ // DeviceType enum: UNKNOWN=0, PHONE=1, TABLET=2, DESKTOP=3, TV=4
122
+ is_tablet: this.expoDevice.deviceType === 2, // DeviceType.TABLET = 2
123
+ is_emulator: !this.expoDevice.isDevice,
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Get device info using react-native-device-info (for bare RN)
129
+ */
130
+ private static async getAppInfoFromDeviceInfo(): Promise<DeviceAppInfo> {
131
+ const [version, buildNumber, uniqueId, bundleId] = await Promise.all([
132
+ this.deviceInfo.getVersion(),
133
+ this.deviceInfo.getBuildNumber(),
134
+ this.deviceInfo.getUniqueId(),
135
+ this.deviceInfo.getBundleId(),
136
+ ]);
137
+
138
+ return {
139
+ // App information
140
+ app_version: version,
141
+ build_number: buildNumber,
142
+ package_name: bundleId,
143
+
144
+ // Device information
145
+ device_model_name: this.deviceInfo.getModel(),
146
+ device_manufacturer: this.deviceInfo.getManufacturerSync(),
147
+ device_id: uniqueId,
148
+
149
+ // OS information
150
+ os_name: Platform.OS,
151
+ os_version: this.deviceInfo.getSystemVersion(),
152
+
153
+ // Additional useful info
154
+ is_tablet: this.deviceInfo.isTablet(),
155
+ is_emulator: await this.deviceInfo.isEmulator(),
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Safe default values when no device info library is available
161
+ */
162
+ private static getSafeDefaults(): DeviceAppInfo {
163
+ return {
164
+ app_version: 'unknown',
165
+ build_number: 'unknown',
166
+ package_name: 'unknown',
167
+ device_model_name: 'unknown',
168
+ device_manufacturer: 'unknown',
169
+ device_id: 'unknown',
170
+ os_name: Platform.OS,
171
+ os_version: Platform.Version?.toString() || 'unknown',
172
+ is_tablet: false,
173
+ is_emulator: false,
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Get app version in format: "1.0.0 (123)"
179
+ * Combines version and build number for better tracking
180
+ */
181
+ static async getFormattedAppVersion(): Promise<string> {
182
+ try {
183
+ const info = await this.getAppInfo();
184
+ return `${info.app_version} (${info.build_number})`;
185
+ } catch (error) {
186
+ Logger.logError('Error getting formatted app version:', error);
187
+ return 'unknown';
188
+ }
189
+ }
190
+ }