@quiltt/react-native 4.5.1 → 5.0.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @quiltt/react-native
2
2
 
3
+ ## 5.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - [#394](https://github.com/quiltt/quiltt-js/pull/394) [`2ba646a`](https://github.com/quiltt/quiltt-js/commit/2ba646a2efcb7bef7949dab74778ab1c3babdb84) Thanks [@zubairaziz](https://github.com/zubairaziz)! - Migrate Apollo Client to v4
8
+
9
+ ### Minor Changes
10
+
11
+ - [#395](https://github.com/quiltt/quiltt-js/pull/395) [`f635500`](https://github.com/quiltt/quiltt-js/commit/f635500f17ab8a76aa0fb87ed7f4971e63a93f12) Thanks [@zubairaziz](https://github.com/zubairaziz)! - Enhanced SDK telemetry with standardized User-Agent headers
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies [[`f635500`](https://github.com/quiltt/quiltt-js/commit/f635500f17ab8a76aa0fb87ed7f4971e63a93f12), [`2ba646a`](https://github.com/quiltt/quiltt-js/commit/2ba646a2efcb7bef7949dab74778ab1c3babdb84)]:
16
+ - @quiltt/react@5.0.0
17
+ - @quiltt/core@5.0.0
18
+
3
19
  ## 4.5.1
4
20
 
5
21
  ### Patch Changes
package/README.md CHANGED
@@ -9,7 +9,7 @@ For general project information and contributing guidelines, see the [main repos
9
9
 
10
10
  ## Installation
11
11
 
12
- `@quiltt/react-native` expects `react`, `react-native`,`react-native-webview`, `base-64` and `react-native-url-polyfill` as peer dependencies.
12
+ `@quiltt/react-native` expects `react`, `react-native`, `react-native-webview`, `base-64` and `react-native-url-polyfill` as peer dependencies.
13
13
 
14
14
  With `npm`:
15
15
 
package/dist/index.d.ts CHANGED
@@ -1,14 +1,26 @@
1
1
  export * from '@quiltt/core';
2
- import { ConnectorSDKCallbacks } from '@quiltt/react';
3
- export { QuilttAuthProvider, QuilttProvider, QuilttSettingsProvider, useQuilttClient, useQuilttConnector, useQuilttSession, useQuilttSettings, useSession, useStorage } from '@quiltt/react';
2
+ import { ConnectorSDKCallbacks, QuilttAuthProviderProps as QuilttAuthProviderProps$1, QuilttSettingsProviderProps } from '@quiltt/react';
3
+ export { ApolloQueryResult, DocumentNode, ErrorPolicy, FetchPolicy, MutationHookOptions, MutationResult, NormalizedCacheObject, OperationVariables, QueryHookOptions, QueryResult, QuilttSettingsProvider, SubscriptionHookOptions, SubscriptionResult, TypedDocumentNode, WatchQueryFetchPolicy, useLazyQuery, useMutation, useQuery, useQuilttClient, useQuilttConnector, useQuilttSession, useQuilttSettings, useSession, useStorage, useSubscription } from '@quiltt/react';
4
4
  import * as react from 'react';
5
+ import { FC } from 'react';
5
6
  import { URL } from 'react-native-url-polyfill';
7
+ import { NoticeTransportPayload } from '@honeybadger-io/core/build/src/types';
8
+
9
+ declare class ErrorReporter {
10
+ private noticeUrl;
11
+ private apiKey;
12
+ private logger;
13
+ private userAgent;
14
+ constructor(userAgent: string);
15
+ notify(error: Error, context?: any): Promise<void>;
16
+ buildPayload(error: Error, localContext?: {}): Promise<Partial<NoticeTransportPayload>>;
17
+ }
6
18
 
7
19
  type PreFlightCheck = {
8
20
  checked: boolean;
9
21
  error?: string;
10
22
  };
11
- declare const checkConnectorUrl: (connectorUrl: string, retryCount?: number) => Promise<PreFlightCheck>;
23
+ declare const checkConnectorUrl: (connectorUrl: string, errorReporter: ErrorReporter, retryCount?: number) => Promise<PreFlightCheck>;
12
24
  /**
13
25
  * Handle opening OAuth URLs with proper encoding detection and normalization
14
26
  */
@@ -24,5 +36,24 @@ declare const QuilttConnector: react.ForwardRefExoticComponent<{
24
36
  testId?: string;
25
37
  } & ConnectorSDKCallbacks & react.RefAttributes<QuilttConnectorHandle>>;
26
38
 
27
- export { QuilttConnector, checkConnectorUrl, handleOAuthUrl };
28
- export type { PreFlightCheck, QuilttConnectorHandle };
39
+ type QuilttAuthProviderProps = QuilttAuthProviderProps$1;
40
+ /**
41
+ * React Native-specific QuilttAuthProvider that injects platform information
42
+ * into the GraphQL client's User-Agent header.
43
+ *
44
+ * If a token is provided, will validate the token against the api and then import
45
+ * it into trusted storage. While this process is happening, the component is put
46
+ * into a loading state and the children are not rendered to prevent race conditions
47
+ * from triggering within the transitionary state.
48
+ */
49
+ declare const QuilttAuthProvider: FC<QuilttAuthProviderProps>;
50
+
51
+ type QuilttProviderProps = QuilttSettingsProviderProps & QuilttAuthProviderProps;
52
+ /**
53
+ * React Native-specific QuilttProvider that combines settings and auth providers
54
+ * with platform-specific GraphQL client configuration.
55
+ */
56
+ declare const QuilttProvider: FC<QuilttProviderProps>;
57
+
58
+ export { QuilttAuthProvider, QuilttConnector, QuilttProvider, checkConnectorUrl, handleOAuthUrl };
59
+ export type { PreFlightCheck, QuilttAuthProviderProps, QuilttConnectorHandle };
package/dist/index.js CHANGED
@@ -1,15 +1,17 @@
1
1
  import { decode } from 'base-64';
2
+ import { getUserAgent as getUserAgent$1, QuilttClient, InMemoryCache, createVersionLink } from '@quiltt/core';
2
3
  export * from '@quiltt/core';
3
- import { useQuilttSession, ConnectorSDKEventType } from '@quiltt/react';
4
- export { QuilttAuthProvider, QuilttProvider, QuilttSettingsProvider, useQuilttClient, useQuilttConnector, useQuilttSession, useQuilttSettings, useSession, useStorage } from '@quiltt/react';
4
+ import { useQuilttSession, ConnectorSDKEventType, QuilttAuthProvider as QuilttAuthProvider$1, QuilttSettingsProvider } from '@quiltt/react';
5
+ export { QuilttSettingsProvider, useLazyQuery, useMutation, useQuery, useQuilttClient, useQuilttConnector, useQuilttSession, useQuilttSettings, useSession, useStorage, useSubscription } from '@quiltt/react';
5
6
  import { jsx, jsxs } from 'react/jsx-runtime';
6
- import { forwardRef, useRef, useState, useCallback, useMemo, useEffect, useImperativeHandle } from 'react';
7
- import { StyleSheet, StatusBar, Platform, SafeAreaView, View, Text, Pressable, ActivityIndicator, Linking } from 'react-native';
7
+ import React, { forwardRef, useRef, useState, useEffect, useCallback, useMemo, useImperativeHandle } from 'react';
8
+ import { Platform, StyleSheet, StatusBar, SafeAreaView, View, Text, Pressable, ActivityIndicator, Linking } from 'react-native';
8
9
  import { URL } from 'react-native-url-polyfill';
9
10
  import { WebView } from 'react-native-webview';
10
11
  import { generateStackTrace, makeBacktrace, getCauses } from '@honeybadger-io/core/build/src/util';
12
+ import __node_cjsModule from 'node:module';
11
13
 
12
- var version = "4.5.1";
14
+ var version = "5.0.0";
13
15
 
14
16
  // Custom Error Reporter to avoid hooking into or colliding with a client's Honeybadger singleton
15
17
  const notifier = {
@@ -18,21 +20,18 @@ const notifier = {
18
20
  version: version
19
21
  };
20
22
  class ErrorReporter {
21
- constructor(platform){
23
+ constructor(userAgent){
22
24
  this.noticeUrl = 'https://api.honeybadger.io/v1/notices';
23
25
  this.apiKey = process.env.HONEYBADGER_API_KEY_REACT_NATIVE || '';
24
- this.clientName = 'react-native-sdk';
25
- this.clientVersion = version;
26
- this.platform = platform;
27
26
  this.logger = console;
28
- this.userAgent = `${this.clientName} ${this.clientVersion}; ${this.platform}`;
27
+ this.userAgent = userAgent;
29
28
  }
30
29
  async notify(error, context) {
31
30
  const headers = {
32
31
  'X-API-Key': this.apiKey,
33
32
  'Content-Type': 'application/json',
34
33
  Accept: 'application/json',
35
- 'User-Agent': `${this.clientName} ${this.clientVersion}; ${this.platform}`
34
+ 'User-Agent': this.userAgent
36
35
  };
37
36
  const payload = await this.buildPayload(error, context);
38
37
  const method = 'POST';
@@ -82,7 +81,7 @@ class ErrorReporter {
82
81
  project_root: notice.projectRoot,
83
82
  environment_name: this.userAgent,
84
83
  revision: version,
85
- hostname: this.platform,
84
+ hostname: this.userAgent,
86
85
  time: new Date().toUTCString()
87
86
  },
88
87
  details: notice.details || {}
@@ -95,6 +94,96 @@ const getErrorMessage = (responseStatus, error)=>{
95
94
  return responseStatus ? `An error occurred loading the Connector. Response status: ${responseStatus}` : 'An error occurred while checking the Connector URL';
96
95
  };
97
96
 
97
+ const require$1 = __node_cjsModule.createRequire(import.meta.url);
98
+
99
+ // Optionally import react-native-device-info if available
100
+ let DeviceInfo = null;
101
+ try {
102
+ DeviceInfo = require$1('react-native-device-info').default;
103
+ } catch {
104
+ // react-native-device-info is not installed - will use fallback
105
+ }
106
+ /**
107
+ * Gets the React version from the runtime
108
+ */ const getReactVersion = ()=>{
109
+ return React.version || 'unknown';
110
+ };
111
+ /**
112
+ * Gets the React Native version from Platform constants
113
+ */ const getReactNativeVersion = ()=>{
114
+ try {
115
+ const rnVersion = Platform.constants?.reactNativeVersion;
116
+ if (rnVersion) {
117
+ return `${rnVersion.major}.${rnVersion.minor}.${rnVersion.patch}`;
118
+ }
119
+ } catch (error) {
120
+ console.warn('Failed to get React Native version:', error);
121
+ }
122
+ return 'unknown';
123
+ };
124
+ /**
125
+ * Gets OS information (platform and version)
126
+ */ const getOSInfo = ()=>{
127
+ try {
128
+ const os = Platform.OS // 'ios' or 'android'
129
+ ;
130
+ const osVersion = Platform.Version // string (iOS) or number (Android)
131
+ ;
132
+ // Map platform names to correct capitalization
133
+ const platformNames = {
134
+ ios: 'iOS',
135
+ android: 'Android'
136
+ };
137
+ const osName = platformNames[os] || 'Unknown';
138
+ return `${osName}/${osVersion}`;
139
+ } catch (error) {
140
+ console.warn('Failed to get OS info:', error);
141
+ return 'Unknown/Unknown';
142
+ }
143
+ };
144
+ /**
145
+ * Gets device model information
146
+ * Falls back to 'Unknown' if react-native-device-info is not installed
147
+ */ const getDeviceModel = async ()=>{
148
+ if (!DeviceInfo) {
149
+ return 'Unknown';
150
+ }
151
+ try {
152
+ const model = await DeviceInfo.getModel();
153
+ return model || 'Unknown';
154
+ } catch (error) {
155
+ console.warn('Failed to get device model:', error);
156
+ return 'Unknown';
157
+ }
158
+ };
159
+ /**
160
+ * Generates platform information string for React Native
161
+ * Format: React/<version>; ReactNative/<version>; <OS>/<version>; <device-model>
162
+ */ const getPlatformInfo = async ()=>{
163
+ const reactVersion = getReactVersion();
164
+ const rnVersion = getReactNativeVersion();
165
+ const osInfo = getOSInfo();
166
+ const deviceModel = await getDeviceModel();
167
+ return `React/${reactVersion}; ReactNative/${rnVersion}; ${osInfo}; ${deviceModel}`;
168
+ };
169
+ /**
170
+ * Synchronously generates platform information string for React Native
171
+ * Format: React/<version>; ReactNative/<version>; <OS>/<version>; Unknown
172
+ * Note: Device model is set to 'Unknown' since it requires async DeviceInfo call
173
+ */ const getPlatformInfoSync = ()=>{
174
+ const reactVersion = getReactVersion();
175
+ const rnVersion = getReactNativeVersion();
176
+ const osInfo = getOSInfo();
177
+ return `React/${reactVersion}; ReactNative/${rnVersion}; ${osInfo}; Unknown`;
178
+ };
179
+ /**
180
+ * Generates User-Agent string for React Native SDK
181
+ * Format: Quiltt/<sdk-version> (React/<version>; ReactNative/<version>; <OS>/<version>; <device-model>)
182
+ */ const getUserAgent = async (sdkVersion)=>{
183
+ const platformInfo = await getPlatformInfo();
184
+ return getUserAgent$1(sdkVersion, platformInfo);
185
+ };
186
+
98
187
  /**
99
188
  * Checks if a string appears to be already URL encoded
100
189
  * @param str The string to check
@@ -254,7 +343,6 @@ const LoadingScreen = ({ testId })=>/*#__PURE__*/ jsx(AndroidSafeAreaView, {
254
343
  })
255
344
  });
256
345
 
257
- const errorReporter = new ErrorReporter(`${Platform.OS} ${Platform.Version}`);
258
346
  const PREFLIGHT_RETRY_COUNT = 3;
259
347
  const parseMetadata = (url, connectorId)=>{
260
348
  const metadata = {
@@ -270,7 +358,7 @@ const parseMetadata = (url, connectorId)=>{
270
358
  };
271
359
  return metadata;
272
360
  };
273
- const checkConnectorUrl = async (connectorUrl, retryCount = 0)=>{
361
+ const checkConnectorUrl = async (connectorUrl, errorReporter, retryCount = 0)=>{
274
362
  let responseStatus;
275
363
  try {
276
364
  const response = await fetch(connectorUrl);
@@ -300,7 +388,7 @@ const checkConnectorUrl = async (connectorUrl, retryCount = 0)=>{
300
388
  const delay = 50 * 2 ** retryCount;
301
389
  await new Promise((resolve)=>setTimeout(resolve, delay));
302
390
  console.log(`Retrying connection... Attempt ${retryCount + 1}`);
303
- return checkConnectorUrl(connectorUrl, retryCount + 1);
391
+ return checkConnectorUrl(connectorUrl, errorReporter, retryCount + 1);
304
392
  }
305
393
  // Report error after retries exhausted
306
394
  const errorMessage = getErrorMessage(responseStatus, error);
@@ -355,6 +443,23 @@ const QuilttConnector = /*#__PURE__*/ forwardRef(({ connectorId, connectionId, i
355
443
  const [preFlightCheck, setPreFlightCheck] = useState({
356
444
  checked: false
357
445
  });
446
+ const [errorReporter, setErrorReporter] = useState(null);
447
+ const [userAgent, setUserAgent] = useState('');
448
+ // Initialize error reporter and user agent
449
+ useEffect(()=>{
450
+ let mounted = true;
451
+ const init = async ()=>{
452
+ const agent = await getUserAgent(version);
453
+ if (mounted) {
454
+ setUserAgent(agent);
455
+ setErrorReporter(new ErrorReporter(agent));
456
+ }
457
+ };
458
+ init();
459
+ return ()=>{
460
+ mounted = false;
461
+ };
462
+ }, []);
358
463
  // Script to disable scrolling on header
359
464
  const disableHeaderScrollScript = `
360
465
  (function() {
@@ -380,10 +485,11 @@ const QuilttConnector = /*#__PURE__*/ forwardRef(({ connectorId, connectionId, i
380
485
  oauthRedirectUrl
381
486
  ]);
382
487
  const connectorUrl = useMemo(()=>{
488
+ if (!userAgent) return null;
383
489
  const url = new URL(`https://${connectorId}.quiltt.app`);
384
490
  // For normal parameters, just append them directly
385
491
  url.searchParams.append('mode', 'webview');
386
- url.searchParams.append('agent', `react-native-${version}`);
492
+ url.searchParams.append('agent', userAgent);
387
493
  // For the oauth_redirect_url, we need to be careful
388
494
  // If it's already encoded, we need to decode it once to prevent
389
495
  // the automatic encoding that happens with searchParams.append
@@ -396,18 +502,20 @@ const QuilttConnector = /*#__PURE__*/ forwardRef(({ connectorId, connectionId, i
396
502
  return url.toString();
397
503
  }, [
398
504
  connectorId,
399
- safeOAuthRedirectUrl
505
+ safeOAuthRedirectUrl,
506
+ userAgent
400
507
  ]);
401
508
  useEffect(()=>{
402
- if (preFlightCheck.checked) return;
509
+ if (preFlightCheck.checked || !connectorUrl || !errorReporter) return;
403
510
  const fetchDataAndSetState = async ()=>{
404
- const connectorUrlStatus = await checkConnectorUrl(connectorUrl);
511
+ const connectorUrlStatus = await checkConnectorUrl(connectorUrl, errorReporter);
405
512
  setPreFlightCheck(connectorUrlStatus);
406
513
  };
407
514
  fetchDataAndSetState();
408
515
  }, [
409
516
  connectorUrl,
410
- preFlightCheck
517
+ preFlightCheck,
518
+ errorReporter
411
519
  ]);
412
520
  const initInjectedJavaScript = useCallback(()=>{
413
521
  const script = `\
@@ -573,7 +681,7 @@ const QuilttConnector = /*#__PURE__*/ forwardRef(({ connectorId, connectionId, i
573
681
  }
574
682
  }
575
683
  }), []);
576
- if (!preFlightCheck.checked) {
684
+ if (!preFlightCheck.checked || !connectorUrl) {
577
685
  return /*#__PURE__*/ jsx(LoadingScreen, {
578
686
  testId: "loading-screen"
579
687
  });
@@ -641,10 +749,53 @@ const styles = StyleSheet.create({
641
749
  });
642
750
  QuilttConnector.displayName = 'QuilttConnector';
643
751
 
752
+ /**
753
+ * React Native-specific QuilttAuthProvider that injects platform information
754
+ * into the GraphQL client's User-Agent header.
755
+ *
756
+ * If a token is provided, will validate the token against the api and then import
757
+ * it into trusted storage. While this process is happening, the component is put
758
+ * into a loading state and the children are not rendered to prevent race conditions
759
+ * from triggering within the transitionary state.
760
+ */ const QuilttAuthProvider = ({ graphqlClient, token, children })=>{
761
+ // Create React Native-specific GraphQL client with platform info if no custom client provided
762
+ const platformClient = useMemo(()=>{
763
+ if (graphqlClient) {
764
+ return graphqlClient;
765
+ }
766
+ const platformInfo = getPlatformInfoSync();
767
+ return new QuilttClient({
768
+ cache: new InMemoryCache(),
769
+ versionLink: createVersionLink(platformInfo)
770
+ });
771
+ }, [
772
+ graphqlClient
773
+ ]);
774
+ return /*#__PURE__*/ jsx(QuilttAuthProvider$1, {
775
+ token: token,
776
+ graphqlClient: platformClient,
777
+ children: children
778
+ });
779
+ };
780
+
781
+ /**
782
+ * React Native-specific QuilttProvider that combines settings and auth providers
783
+ * with platform-specific GraphQL client configuration.
784
+ */ const QuilttProvider = ({ clientId, graphqlClient, token, children })=>{
785
+ return /*#__PURE__*/ jsx(QuilttSettingsProvider, {
786
+ clientId: clientId,
787
+ children: /*#__PURE__*/ jsx(QuilttAuthProvider, {
788
+ token: token,
789
+ graphqlClient: graphqlClient,
790
+ children: children
791
+ })
792
+ });
793
+ };
794
+
644
795
  // Hermes doesn't have atob
645
796
  // https://github.com/facebook/hermes/issues/1178
646
797
  if (!global.atob) {
647
798
  global.atob = decode;
648
799
  }
649
800
 
650
- export { QuilttConnector, checkConnectorUrl, handleOAuthUrl };
801
+ export { QuilttAuthProvider, QuilttConnector, QuilttProvider, checkConnectorUrl, handleOAuthUrl };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quiltt/react-native",
3
- "version": "4.5.1",
3
+ "version": "5.0.0",
4
4
  "description": "React Native Components for Quiltt Connector",
5
5
  "homepage": "https://github.com/quiltt/quiltt-js/tree/main/packages/react-native#readme",
6
6
  "repository": {
@@ -28,24 +28,24 @@
28
28
  ],
29
29
  "main": "dist/index.js",
30
30
  "dependencies": {
31
- "@honeybadger-io/core": "6.6.0",
31
+ "@honeybadger-io/core": "6.7.2",
32
32
  "lodash.debounce": "4.0.8",
33
- "@quiltt/react": "4.5.1",
34
- "@quiltt/core": "4.5.1"
33
+ "react-native-device-info": "15.0.1",
34
+ "@quiltt/core": "5.0.0",
35
+ "@quiltt/react": "5.0.0"
35
36
  },
36
37
  "devDependencies": {
37
- "@biomejs/biome": "2.3.8",
38
+ "@biomejs/biome": "2.3.13",
38
39
  "@types/base-64": "1.0.2",
39
40
  "@types/lodash.debounce": "4.0.9",
40
- "@types/node": "22.19.2",
41
- "@types/react": "19.2.7",
41
+ "@types/node": "24.10.9",
42
+ "@types/react": "19.2.10",
42
43
  "base-64": "1.0.0",
43
- "bunchee": "6.6.2",
44
- "react": "19.2.3",
44
+ "bunchee": "6.9.4",
45
+ "react": "19.2.4",
45
46
  "react-native": "0.81.5",
46
47
  "react-native-url-polyfill": "3.0.0",
47
48
  "react-native-webview": "13.15.0",
48
- "react-test-renderer": "19.2.3",
49
49
  "rimraf": "6.1.2",
50
50
  "typescript": "5.9.3"
51
51
  },
@@ -18,6 +18,7 @@ import type { ShouldStartLoadRequest } from 'react-native-webview/lib/WebViewTyp
18
18
  import {
19
19
  ErrorReporter,
20
20
  getErrorMessage,
21
+ getUserAgent,
21
22
  isEncoded,
22
23
  normalizeUrlEncoding,
23
24
  smartEncodeURIComponent,
@@ -28,7 +29,6 @@ import { AndroidSafeAreaView } from './AndroidSafeAreaView'
28
29
  import { ErrorScreen } from './ErrorScreen'
29
30
  import { LoadingScreen } from './LoadingScreen'
30
31
 
31
- const errorReporter = new ErrorReporter(`${Platform.OS} ${Platform.Version}`)
32
32
  const PREFLIGHT_RETRY_COUNT = 3
33
33
 
34
34
  export type PreFlightCheck = {
@@ -55,6 +55,7 @@ const parseMetadata = (url: URL, connectorId: string): ConnectorSDKCallbackMetad
55
55
 
56
56
  export const checkConnectorUrl = async (
57
57
  connectorUrl: string,
58
+ errorReporter: ErrorReporter,
58
59
  retryCount = 0
59
60
  ): Promise<PreFlightCheck> => {
60
61
  let responseStatus: number | undefined
@@ -85,7 +86,7 @@ export const checkConnectorUrl = async (
85
86
  const delay = 50 * 2 ** retryCount
86
87
  await new Promise((resolve) => setTimeout(resolve, delay))
87
88
  console.log(`Retrying connection... Attempt ${retryCount + 1}`)
88
- return checkConnectorUrl(connectorUrl, retryCount + 1)
89
+ return checkConnectorUrl(connectorUrl, errorReporter, retryCount + 1)
89
90
  }
90
91
 
91
92
  // Report error after retries exhausted
@@ -170,6 +171,25 @@ const QuilttConnector = forwardRef<QuilttConnectorHandle, QuilttConnectorProps>(
170
171
  const webViewRef = useRef<WebView>(null)
171
172
  const { session } = useQuilttSession()
172
173
  const [preFlightCheck, setPreFlightCheck] = useState<PreFlightCheck>({ checked: false })
174
+ const [errorReporter, setErrorReporter] = useState<ErrorReporter | null>(null)
175
+ const [userAgent, setUserAgent] = useState<string>('')
176
+
177
+ // Initialize error reporter and user agent
178
+ useEffect(() => {
179
+ let mounted = true
180
+ const init = async () => {
181
+ const agent = await getUserAgent(version)
182
+ if (mounted) {
183
+ setUserAgent(agent)
184
+ setErrorReporter(new ErrorReporter(agent))
185
+ }
186
+ }
187
+ init()
188
+
189
+ return () => {
190
+ mounted = false
191
+ }
192
+ }, [])
173
193
 
174
194
  // Script to disable scrolling on header
175
195
  const disableHeaderScrollScript = `
@@ -197,11 +217,13 @@ const QuilttConnector = forwardRef<QuilttConnectorHandle, QuilttConnectorProps>(
197
217
  }, [oauthRedirectUrl])
198
218
 
199
219
  const connectorUrl = useMemo(() => {
220
+ if (!userAgent) return null
221
+
200
222
  const url = new URL(`https://${connectorId}.quiltt.app`)
201
223
 
202
224
  // For normal parameters, just append them directly
203
225
  url.searchParams.append('mode', 'webview')
204
- url.searchParams.append('agent', `react-native-${version}`)
226
+ url.searchParams.append('agent', userAgent)
205
227
 
206
228
  // For the oauth_redirect_url, we need to be careful
207
229
  // If it's already encoded, we need to decode it once to prevent
@@ -214,16 +236,16 @@ const QuilttConnector = forwardRef<QuilttConnectorHandle, QuilttConnectorProps>(
214
236
  }
215
237
 
216
238
  return url.toString()
217
- }, [connectorId, safeOAuthRedirectUrl])
239
+ }, [connectorId, safeOAuthRedirectUrl, userAgent])
218
240
 
219
241
  useEffect(() => {
220
- if (preFlightCheck.checked) return
242
+ if (preFlightCheck.checked || !connectorUrl || !errorReporter) return
221
243
  const fetchDataAndSetState = async () => {
222
- const connectorUrlStatus = await checkConnectorUrl(connectorUrl)
244
+ const connectorUrlStatus = await checkConnectorUrl(connectorUrl, errorReporter)
223
245
  setPreFlightCheck(connectorUrlStatus)
224
246
  }
225
247
  fetchDataAndSetState()
226
- }, [connectorUrl, preFlightCheck])
248
+ }, [connectorUrl, preFlightCheck, errorReporter])
227
249
 
228
250
  const initInjectedJavaScript = useCallback(() => {
229
251
  const script = `\
@@ -404,7 +426,7 @@ const QuilttConnector = forwardRef<QuilttConnectorHandle, QuilttConnectorProps>(
404
426
  []
405
427
  )
406
428
 
407
- if (!preFlightCheck.checked) {
429
+ if (!preFlightCheck.checked || !connectorUrl) {
408
430
  return <LoadingScreen testId="loading-screen" />
409
431
  }
410
432
 
package/src/index.ts CHANGED
@@ -7,16 +7,38 @@ if (!global.atob) {
7
7
  }
8
8
 
9
9
  export * from '@quiltt/core'
10
+ // Re-export Apollo Client types (via @quiltt/react)
11
+ export type {
12
+ ApolloQueryResult,
13
+ DocumentNode,
14
+ ErrorPolicy,
15
+ FetchPolicy,
16
+ MutationHookOptions,
17
+ MutationResult,
18
+ NormalizedCacheObject,
19
+ OperationVariables,
20
+ QueryHookOptions,
21
+ QueryResult,
22
+ SubscriptionHookOptions,
23
+ SubscriptionResult,
24
+ TypedDocumentNode,
25
+ WatchQueryFetchPolicy,
26
+ } from '@quiltt/react'
27
+ // Re-export Apollo Client utilities and hooks (via @quiltt/react)
28
+ // Re-export Quiltt-specific providers and hooks
10
29
  export {
11
- QuilttAuthProvider,
12
- QuilttProvider,
13
30
  QuilttSettingsProvider,
31
+ useLazyQuery,
32
+ useMutation,
33
+ useQuery,
14
34
  useQuilttClient,
15
35
  useQuilttConnector,
16
36
  useQuilttSession,
17
37
  useQuilttSettings,
18
38
  useSession,
19
39
  useStorage,
40
+ useSubscription,
20
41
  } from '@quiltt/react'
21
42
 
22
43
  export * from './components'
44
+ export * from './providers'
@@ -0,0 +1,46 @@
1
+ import type { FC } from 'react'
2
+ import { useMemo } from 'react'
3
+
4
+ import { createVersionLink, InMemoryCache, QuilttClient } from '@quiltt/core'
5
+ import type { QuilttAuthProviderProps as ReactQuilttAuthProviderProps } from '@quiltt/react'
6
+ import { QuilttAuthProvider as ReactQuilttAuthProvider } from '@quiltt/react'
7
+
8
+ import { getPlatformInfoSync } from '@/utils/telemetry'
9
+
10
+ export type QuilttAuthProviderProps = ReactQuilttAuthProviderProps
11
+
12
+ /**
13
+ * React Native-specific QuilttAuthProvider that injects platform information
14
+ * into the GraphQL client's User-Agent header.
15
+ *
16
+ * If a token is provided, will validate the token against the api and then import
17
+ * it into trusted storage. While this process is happening, the component is put
18
+ * into a loading state and the children are not rendered to prevent race conditions
19
+ * from triggering within the transitionary state.
20
+ */
21
+ export const QuilttAuthProvider: FC<QuilttAuthProviderProps> = ({
22
+ graphqlClient,
23
+ token,
24
+ children,
25
+ }) => {
26
+ // Create React Native-specific GraphQL client with platform info if no custom client provided
27
+ const platformClient = useMemo(() => {
28
+ if (graphqlClient) {
29
+ return graphqlClient
30
+ }
31
+
32
+ const platformInfo = getPlatformInfoSync()
33
+ return new QuilttClient({
34
+ cache: new InMemoryCache(),
35
+ versionLink: createVersionLink(platformInfo),
36
+ })
37
+ }, [graphqlClient])
38
+
39
+ return (
40
+ <ReactQuilttAuthProvider token={token} graphqlClient={platformClient}>
41
+ {children}
42
+ </ReactQuilttAuthProvider>
43
+ )
44
+ }
45
+
46
+ export default QuilttAuthProvider
@@ -0,0 +1,30 @@
1
+ import type { FC } from 'react'
2
+
3
+ import type { QuilttSettingsProviderProps } from '@quiltt/react'
4
+ import { QuilttSettingsProvider } from '@quiltt/react'
5
+
6
+ import type { QuilttAuthProviderProps } from './QuilttAuthProvider'
7
+ import { QuilttAuthProvider } from './QuilttAuthProvider'
8
+
9
+ type QuilttProviderProps = QuilttSettingsProviderProps & QuilttAuthProviderProps
10
+
11
+ /**
12
+ * React Native-specific QuilttProvider that combines settings and auth providers
13
+ * with platform-specific GraphQL client configuration.
14
+ */
15
+ export const QuilttProvider: FC<QuilttProviderProps> = ({
16
+ clientId,
17
+ graphqlClient,
18
+ token,
19
+ children,
20
+ }) => {
21
+ return (
22
+ <QuilttSettingsProvider clientId={clientId}>
23
+ <QuilttAuthProvider token={token} graphqlClient={graphqlClient}>
24
+ {children}
25
+ </QuilttAuthProvider>
26
+ </QuilttSettingsProvider>
27
+ )
28
+ }
29
+
30
+ export default QuilttProvider
@@ -0,0 +1,2 @@
1
+ export * from './QuilttAuthProvider'
2
+ export * from './QuilttProvider'
@@ -17,20 +17,14 @@ type HoneybadgerResponseData = {
17
17
  class ErrorReporter {
18
18
  private noticeUrl: string
19
19
  private apiKey: string
20
- private clientName: string
21
- private clientVersion: string
22
- private platform: string
23
20
  private logger: Console
24
21
  private userAgent: string
25
22
 
26
- constructor(platform: string) {
23
+ constructor(userAgent: string) {
27
24
  this.noticeUrl = 'https://api.honeybadger.io/v1/notices'
28
25
  this.apiKey = process.env.HONEYBADGER_API_KEY_REACT_NATIVE || ''
29
- this.clientName = 'react-native-sdk'
30
- this.clientVersion = version
31
- this.platform = platform
32
26
  this.logger = console
33
- this.userAgent = `${this.clientName} ${this.clientVersion}; ${this.platform}`
27
+ this.userAgent = userAgent
34
28
  }
35
29
 
36
30
  async notify(error: Error, context?: any): Promise<void> {
@@ -38,7 +32,7 @@ class ErrorReporter {
38
32
  'X-API-Key': this.apiKey,
39
33
  'Content-Type': 'application/json',
40
34
  Accept: 'application/json',
41
- 'User-Agent': `${this.clientName} ${this.clientVersion}; ${this.platform}`,
35
+ 'User-Agent': this.userAgent,
42
36
  }
43
37
 
44
38
  const payload = await this.buildPayload(error, context)
@@ -92,7 +86,7 @@ class ErrorReporter {
92
86
  project_root: notice.projectRoot,
93
87
  environment_name: this.userAgent,
94
88
  revision: version,
95
- hostname: this.platform,
89
+ hostname: this.userAgent,
96
90
  time: new Date().toUTCString(),
97
91
  },
98
92
  details: notice.details || {},
@@ -1,2 +1,3 @@
1
1
  export * from './error'
2
+ export * from './telemetry'
2
3
  export * from './url'
@@ -0,0 +1,109 @@
1
+ import React from 'react'
2
+ import { Platform } from 'react-native'
3
+
4
+ import { getUserAgent as coreGetUserAgent } from '@quiltt/core'
5
+
6
+ // Optionally import react-native-device-info if available
7
+ let DeviceInfo: any = null
8
+ try {
9
+ DeviceInfo = require('react-native-device-info').default
10
+ } catch {
11
+ // react-native-device-info is not installed - will use fallback
12
+ }
13
+
14
+ /**
15
+ * Gets the React version from the runtime
16
+ */
17
+ export const getReactVersion = (): string => {
18
+ return React.version || 'unknown'
19
+ }
20
+
21
+ /**
22
+ * Gets the React Native version from Platform constants
23
+ */
24
+ export const getReactNativeVersion = (): string => {
25
+ try {
26
+ const rnVersion = Platform.constants?.reactNativeVersion
27
+ if (rnVersion) {
28
+ return `${rnVersion.major}.${rnVersion.minor}.${rnVersion.patch}`
29
+ }
30
+ } catch (error) {
31
+ console.warn('Failed to get React Native version:', error)
32
+ }
33
+ return 'unknown'
34
+ }
35
+
36
+ /**
37
+ * Gets OS information (platform and version)
38
+ */
39
+ export const getOSInfo = (): string => {
40
+ try {
41
+ const os = Platform.OS // 'ios' or 'android'
42
+ const osVersion = Platform.Version // string (iOS) or number (Android)
43
+
44
+ // Map platform names to correct capitalization
45
+ const platformNames: Record<string, string> = {
46
+ ios: 'iOS',
47
+ android: 'Android',
48
+ }
49
+ const osName = platformNames[os] || 'Unknown'
50
+
51
+ return `${osName}/${osVersion}`
52
+ } catch (error) {
53
+ console.warn('Failed to get OS info:', error)
54
+ return 'Unknown/Unknown'
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Gets device model information
60
+ * Falls back to 'Unknown' if react-native-device-info is not installed
61
+ */
62
+ export const getDeviceModel = async (): Promise<string> => {
63
+ if (!DeviceInfo) {
64
+ return 'Unknown'
65
+ }
66
+
67
+ try {
68
+ const model = await DeviceInfo.getModel()
69
+ return model || 'Unknown'
70
+ } catch (error) {
71
+ console.warn('Failed to get device model:', error)
72
+ return 'Unknown'
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Generates platform information string for React Native
78
+ * Format: React/<version>; ReactNative/<version>; <OS>/<version>; <device-model>
79
+ */
80
+ export const getPlatformInfo = async (): Promise<string> => {
81
+ const reactVersion = getReactVersion()
82
+ const rnVersion = getReactNativeVersion()
83
+ const osInfo = getOSInfo()
84
+ const deviceModel = await getDeviceModel()
85
+
86
+ return `React/${reactVersion}; ReactNative/${rnVersion}; ${osInfo}; ${deviceModel}`
87
+ }
88
+
89
+ /**
90
+ * Synchronously generates platform information string for React Native
91
+ * Format: React/<version>; ReactNative/<version>; <OS>/<version>; Unknown
92
+ * Note: Device model is set to 'Unknown' since it requires async DeviceInfo call
93
+ */
94
+ export const getPlatformInfoSync = (): string => {
95
+ const reactVersion = getReactVersion()
96
+ const rnVersion = getReactNativeVersion()
97
+ const osInfo = getOSInfo()
98
+
99
+ return `React/${reactVersion}; ReactNative/${rnVersion}; ${osInfo}; Unknown`
100
+ }
101
+
102
+ /**
103
+ * Generates User-Agent string for React Native SDK
104
+ * Format: Quiltt/<sdk-version> (React/<version>; ReactNative/<version>; <OS>/<version>; <device-model>)
105
+ */
106
+ export const getUserAgent = async (sdkVersion: string): Promise<string> => {
107
+ const platformInfo = await getPlatformInfo()
108
+ return coreGetUserAgent(sdkVersion, platformInfo)
109
+ }