@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 +16 -0
- package/README.md +1 -1
- package/dist/index.d.ts +36 -5
- package/dist/index.js +173 -22
- package/package.json +10 -10
- package/src/components/QuilttConnector.tsx +30 -8
- package/src/index.ts +24 -2
- package/src/providers/QuilttAuthProvider.tsx +46 -0
- package/src/providers/QuilttProvider.tsx +30 -0
- package/src/providers/index.ts +2 -0
- package/src/utils/error/ErrorReporter.ts +4 -10
- package/src/utils/index.ts +1 -0
- package/src/utils/telemetry.ts +109 -0
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
|
|
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 {
|
|
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
|
-
|
|
28
|
-
|
|
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 {
|
|
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,
|
|
7
|
-
import { StyleSheet, StatusBar,
|
|
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 = "
|
|
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(
|
|
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 =
|
|
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':
|
|
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.
|
|
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',
|
|
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": "
|
|
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.
|
|
31
|
+
"@honeybadger-io/core": "6.7.2",
|
|
32
32
|
"lodash.debounce": "4.0.8",
|
|
33
|
-
"
|
|
34
|
-
"@quiltt/core": "
|
|
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.
|
|
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": "
|
|
41
|
-
"@types/react": "19.2.
|
|
41
|
+
"@types/node": "24.10.9",
|
|
42
|
+
"@types/react": "19.2.10",
|
|
42
43
|
"base-64": "1.0.0",
|
|
43
|
-
"bunchee": "6.
|
|
44
|
-
"react": "19.2.
|
|
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',
|
|
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
|
|
@@ -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(
|
|
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 =
|
|
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':
|
|
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.
|
|
89
|
+
hostname: this.userAgent,
|
|
96
90
|
time: new Date().toUTCString(),
|
|
97
91
|
},
|
|
98
92
|
details: notice.details || {},
|
package/src/utils/index.ts
CHANGED
|
@@ -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
|
+
}
|