@quiltt/react-native 4.5.1 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +7 -1
- package/dist/components/index.cjs +534 -0
- package/dist/components/index.d.ts +27 -0
- package/dist/components/index.js +530 -0
- package/dist/index.cjs +76 -0
- package/dist/index.d.ts +3 -27
- package/dist/index.js +3 -642
- package/dist/providers/index.cjs +107 -0
- package/dist/providers/index.d.ts +24 -0
- package/dist/providers/index.js +100 -0
- package/dist/utils/index.cjs +221 -0
- package/dist/utils/index.d.ts +66 -0
- package/dist/utils/index.js +201 -0
- package/package.json +30 -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 +31 -88
- package/src/utils/index.ts +1 -0
- package/src/utils/telemetry.ts +97 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import Honeybadger from '@honeybadger-io/react-native';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Platform } from 'react-native';
|
|
4
|
+
import { getUserAgent as getUserAgent$1 } from '@quiltt/core';
|
|
5
|
+
import DeviceInfo from 'react-native-device-info';
|
|
6
|
+
|
|
7
|
+
var version = "5.0.1";
|
|
8
|
+
|
|
9
|
+
// Custom Error Reporter to avoid hooking into or colliding with a client's Honeybadger singleton
|
|
10
|
+
class ErrorReporter {
|
|
11
|
+
constructor(userAgent){
|
|
12
|
+
// Create an isolated Honeybadger instance to avoid colliding with client's singleton
|
|
13
|
+
this.client = Honeybadger.factory({
|
|
14
|
+
apiKey: process.env.HONEYBADGER_API_KEY_REACT_NATIVE || '',
|
|
15
|
+
environment: userAgent,
|
|
16
|
+
revision: version,
|
|
17
|
+
reportData: true,
|
|
18
|
+
enableUncaught: false,
|
|
19
|
+
enableUnhandledRejection: false
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
async notify(error, context) {
|
|
23
|
+
if (!this.client) {
|
|
24
|
+
console.warn('ErrorReporter: Honeybadger client not initialized');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
// Set context for this error report
|
|
29
|
+
if (context) {
|
|
30
|
+
this.client.setContext(context);
|
|
31
|
+
}
|
|
32
|
+
// Notify Honeybadger
|
|
33
|
+
await this.client.notify(error);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error('ErrorReporter: Failed to send error report', err);
|
|
36
|
+
} finally{
|
|
37
|
+
// Clear context after reporting (always runs if context was set)
|
|
38
|
+
if (context) {
|
|
39
|
+
this.client.clear();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const getErrorMessage = (responseStatus, error)=>{
|
|
46
|
+
if (error) return `An error occurred while checking the Connector URL: ${error?.name} \n${error?.message}`;
|
|
47
|
+
return responseStatus ? `An error occurred loading the Connector. Response status: ${responseStatus}` : 'An error occurred while checking the Connector URL';
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Gets the React version from the runtime
|
|
52
|
+
*/ const getReactVersion = ()=>{
|
|
53
|
+
return React.version || 'unknown';
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Gets the React Native version from Platform constants
|
|
57
|
+
*/ const getReactNativeVersion = ()=>{
|
|
58
|
+
try {
|
|
59
|
+
const rnVersion = Platform.constants?.reactNativeVersion;
|
|
60
|
+
if (rnVersion) {
|
|
61
|
+
return `${rnVersion.major}.${rnVersion.minor}.${rnVersion.patch}`;
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.warn('Failed to get React Native version:', error);
|
|
65
|
+
}
|
|
66
|
+
return 'unknown';
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Gets OS information (platform and version)
|
|
70
|
+
*/ const getOSInfo = ()=>{
|
|
71
|
+
try {
|
|
72
|
+
const os = Platform.OS // 'ios' or 'android'
|
|
73
|
+
;
|
|
74
|
+
const osVersion = Platform.Version // string (iOS) or number (Android)
|
|
75
|
+
;
|
|
76
|
+
// Map platform names to correct capitalization
|
|
77
|
+
const platformNames = {
|
|
78
|
+
ios: 'iOS',
|
|
79
|
+
android: 'Android'
|
|
80
|
+
};
|
|
81
|
+
const osName = platformNames[os] || 'Unknown';
|
|
82
|
+
return `${osName}/${osVersion}`;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.warn('Failed to get OS info:', error);
|
|
85
|
+
return 'Unknown/Unknown';
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Gets device model information
|
|
90
|
+
*/ const getDeviceModel = async ()=>{
|
|
91
|
+
try {
|
|
92
|
+
const model = await DeviceInfo.getModel();
|
|
93
|
+
return model || 'Unknown';
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.warn('Failed to get device model:', error);
|
|
96
|
+
return 'Unknown';
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Generates platform information string for React Native
|
|
101
|
+
* Format: React/<version>; ReactNative/<version>; <OS>/<version>; <device-model>
|
|
102
|
+
*/ const getPlatformInfo = async ()=>{
|
|
103
|
+
const reactVersion = getReactVersion();
|
|
104
|
+
const rnVersion = getReactNativeVersion();
|
|
105
|
+
const osInfo = getOSInfo();
|
|
106
|
+
const deviceModel = await getDeviceModel();
|
|
107
|
+
return `React/${reactVersion}; ReactNative/${rnVersion}; ${osInfo}; ${deviceModel}`;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Synchronously generates platform information string for React Native
|
|
111
|
+
* Format: React/<version>; ReactNative/<version>; <OS>/<version>; Unknown
|
|
112
|
+
* Note: Device model is set to 'Unknown' since it requires async DeviceInfo call
|
|
113
|
+
*/ const getPlatformInfoSync = ()=>{
|
|
114
|
+
const reactVersion = getReactVersion();
|
|
115
|
+
const rnVersion = getReactNativeVersion();
|
|
116
|
+
const osInfo = getOSInfo();
|
|
117
|
+
return `React/${reactVersion}; ReactNative/${rnVersion}; ${osInfo}; Unknown`;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Generates User-Agent string for React Native SDK
|
|
121
|
+
* Format: Quiltt/<sdk-version> (React/<version>; ReactNative/<version>; <OS>/<version>; <device-model>)
|
|
122
|
+
*/ const getUserAgent = async (sdkVersion)=>{
|
|
123
|
+
const platformInfo = await getPlatformInfo();
|
|
124
|
+
return getUserAgent$1(sdkVersion, platformInfo);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Checks if a string appears to be already URL encoded
|
|
129
|
+
* @param str The string to check
|
|
130
|
+
* @returns boolean indicating if the string appears to be URL encoded
|
|
131
|
+
*/ const isEncoded = (str)=>{
|
|
132
|
+
// Check for typical URL encoding patterns like %20, %3A, etc.
|
|
133
|
+
const hasEncodedChars = /%[0-9A-F]{2}/i.test(str);
|
|
134
|
+
// Check if double encoding has occurred (e.g., %253A instead of %3A)
|
|
135
|
+
const hasDoubleEncoding = /%25[0-9A-F]{2}/i.test(str);
|
|
136
|
+
// If we have encoded chars but no double encoding, it's likely properly encoded
|
|
137
|
+
return hasEncodedChars && !hasDoubleEncoding;
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Smart URL encoder that ensures a string is encoded exactly once
|
|
141
|
+
* @param str The string to encode
|
|
142
|
+
* @returns A properly URL encoded string
|
|
143
|
+
*/ const smartEncodeURIComponent = (str)=>{
|
|
144
|
+
if (!str) return str;
|
|
145
|
+
// If it's already encoded, return as is
|
|
146
|
+
if (isEncoded(str)) {
|
|
147
|
+
console.log('URL already encoded, skipping encoding');
|
|
148
|
+
return str;
|
|
149
|
+
}
|
|
150
|
+
// Otherwise, encode it
|
|
151
|
+
const encoded = encodeURIComponent(str);
|
|
152
|
+
console.log('URL encoded');
|
|
153
|
+
return encoded;
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Creates a URL with proper parameter encoding
|
|
157
|
+
* @param baseUrl The base URL string
|
|
158
|
+
* @param params Object containing key-value pairs to be added as search params
|
|
159
|
+
* @returns A properly formatted URL string
|
|
160
|
+
*/ const createUrlWithParams = (baseUrl, params)=>{
|
|
161
|
+
try {
|
|
162
|
+
const url = new URL(baseUrl);
|
|
163
|
+
// Add each parameter without additional encoding
|
|
164
|
+
// (URLSearchParams.append will encode them automatically)
|
|
165
|
+
Object.entries(params).forEach(([key, value])=>{
|
|
166
|
+
// Skip undefined or null values
|
|
167
|
+
if (value == null) return;
|
|
168
|
+
// For oauth_redirect_url specifically, ensure it's not double encoded
|
|
169
|
+
if (key === 'oauth_redirect_url' && isEncoded(value)) {
|
|
170
|
+
// Decode once to counteract the automatic encoding that will happen
|
|
171
|
+
const decodedOnce = decodeURIComponent(value);
|
|
172
|
+
url.searchParams.append(key, decodedOnce);
|
|
173
|
+
} else {
|
|
174
|
+
url.searchParams.append(key, value);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
return url.toString();
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error('Error creating URL with params:', error);
|
|
180
|
+
return baseUrl;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* Checks if a string appears to be double-encoded
|
|
185
|
+
*/ const isDoubleEncoded = (str)=>{
|
|
186
|
+
if (!str) return false;
|
|
187
|
+
return /%25[0-9A-F]{2}/i.test(str);
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Normalizes a URL string by decoding it once if it appears to be double-encoded
|
|
191
|
+
*/ const normalizeUrlEncoding = (urlStr)=>{
|
|
192
|
+
if (isDoubleEncoded(urlStr)) {
|
|
193
|
+
console.log('Detected double-encoded URL:', urlStr);
|
|
194
|
+
const normalized = decodeURIComponent(urlStr);
|
|
195
|
+
console.log('Normalized to:', normalized);
|
|
196
|
+
return normalized;
|
|
197
|
+
}
|
|
198
|
+
return urlStr;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export { ErrorReporter, createUrlWithParams, getDeviceModel, getErrorMessage, getOSInfo, getPlatformInfo, getPlatformInfoSync, getReactNativeVersion, getReactVersion, getUserAgent, isEncoded, normalizeUrlEncoding, smartEncodeURIComponent };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quiltt/react-native",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.1",
|
|
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": {
|
|
@@ -16,7 +16,27 @@
|
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
18
18
|
"types": "./dist/index.d.ts",
|
|
19
|
+
"require": "./dist/index.cjs",
|
|
20
|
+
"import": "./dist/index.js",
|
|
19
21
|
"default": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./components": {
|
|
24
|
+
"types": "./dist/components/index.d.ts",
|
|
25
|
+
"require": "./dist/components/index.cjs",
|
|
26
|
+
"import": "./dist/components/index.js",
|
|
27
|
+
"default": "./dist/components/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./providers": {
|
|
30
|
+
"types": "./dist/providers/index.d.ts",
|
|
31
|
+
"require": "./dist/providers/index.cjs",
|
|
32
|
+
"import": "./dist/providers/index.js",
|
|
33
|
+
"default": "./dist/providers/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./utils": {
|
|
36
|
+
"types": "./dist/utils/index.d.ts",
|
|
37
|
+
"require": "./dist/utils/index.cjs",
|
|
38
|
+
"import": "./dist/utils/index.js",
|
|
39
|
+
"default": "./dist/utils/index.js"
|
|
20
40
|
}
|
|
21
41
|
},
|
|
22
42
|
"module": "./dist/index.js",
|
|
@@ -28,24 +48,24 @@
|
|
|
28
48
|
],
|
|
29
49
|
"main": "dist/index.js",
|
|
30
50
|
"dependencies": {
|
|
31
|
-
"@honeybadger-io/
|
|
51
|
+
"@honeybadger-io/react-native": "6.4.7",
|
|
32
52
|
"lodash.debounce": "4.0.8",
|
|
33
|
-
"
|
|
34
|
-
"@quiltt/
|
|
53
|
+
"react-native-device-info": "15.0.1",
|
|
54
|
+
"@quiltt/react": "5.0.1",
|
|
55
|
+
"@quiltt/core": "5.0.1"
|
|
35
56
|
},
|
|
36
57
|
"devDependencies": {
|
|
37
|
-
"@biomejs/biome": "2.3.
|
|
58
|
+
"@biomejs/biome": "2.3.14",
|
|
38
59
|
"@types/base-64": "1.0.2",
|
|
39
60
|
"@types/lodash.debounce": "4.0.9",
|
|
40
|
-
"@types/node": "
|
|
41
|
-
"@types/react": "19.2.
|
|
61
|
+
"@types/node": "24.10.10",
|
|
62
|
+
"@types/react": "19.2.11",
|
|
42
63
|
"base-64": "1.0.0",
|
|
43
|
-
"bunchee": "6.
|
|
44
|
-
"react": "19.2.
|
|
64
|
+
"bunchee": "6.9.4",
|
|
65
|
+
"react": "19.2.4",
|
|
45
66
|
"react-native": "0.81.5",
|
|
46
67
|
"react-native-url-polyfill": "3.0.0",
|
|
47
68
|
"react-native-webview": "13.15.0",
|
|
48
|
-
"react-test-renderer": "19.2.3",
|
|
49
69
|
"rimraf": "6.1.2",
|
|
50
70
|
"typescript": "5.9.3"
|
|
51
71
|
},
|
|
@@ -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
|
|
@@ -1,101 +1,44 @@
|
|
|
1
1
|
// Custom Error Reporter to avoid hooking into or colliding with a client's Honeybadger singleton
|
|
2
|
-
import
|
|
3
|
-
import { generateStackTrace, getCauses, makeBacktrace } from '@honeybadger-io/core/build/src/util'
|
|
2
|
+
import Honeybadger from '@honeybadger-io/react-native'
|
|
4
3
|
|
|
5
4
|
import { version } from '@/version'
|
|
6
5
|
|
|
7
|
-
const notifier = {
|
|
8
|
-
name: 'Quiltt React Native SDK Reporter',
|
|
9
|
-
url: 'https://www.quiltt.dev/connector/sdk/react-native',
|
|
10
|
-
version: version,
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
type HoneybadgerResponseData = {
|
|
14
|
-
id: string
|
|
15
|
-
}
|
|
16
|
-
|
|
17
6
|
class ErrorReporter {
|
|
18
|
-
private
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
this.clientVersion = version
|
|
31
|
-
this.platform = platform
|
|
32
|
-
this.logger = console
|
|
33
|
-
this.userAgent = `${this.clientName} ${this.clientVersion}; ${this.platform}`
|
|
7
|
+
private client: typeof Honeybadger
|
|
8
|
+
|
|
9
|
+
constructor(userAgent: string) {
|
|
10
|
+
// Create an isolated Honeybadger instance to avoid colliding with client's singleton
|
|
11
|
+
this.client = Honeybadger.factory({
|
|
12
|
+
apiKey: process.env.HONEYBADGER_API_KEY_REACT_NATIVE || '',
|
|
13
|
+
environment: userAgent,
|
|
14
|
+
revision: version,
|
|
15
|
+
reportData: true,
|
|
16
|
+
enableUncaught: false, // Don't hook into global error handlers
|
|
17
|
+
enableUnhandledRejection: false, // Don't hook into global rejection handlers
|
|
18
|
+
})
|
|
34
19
|
}
|
|
35
20
|
|
|
36
21
|
async notify(error: Error, context?: any): Promise<void> {
|
|
37
|
-
|
|
38
|
-
'
|
|
39
|
-
|
|
40
|
-
Accept: 'application/json',
|
|
41
|
-
'User-Agent': `${this.clientName} ${this.clientVersion}; ${this.platform}`,
|
|
22
|
+
if (!this.client) {
|
|
23
|
+
console.warn('ErrorReporter: Honeybadger client not initialized')
|
|
24
|
+
return
|
|
42
25
|
}
|
|
43
26
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
.then((data: HoneybadgerResponseData) => {
|
|
60
|
-
if (data) {
|
|
61
|
-
this.logger.info(`Error report sent ⚡ https://app.honeybadger.io/notice/${data?.id}`)
|
|
62
|
-
}
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async buildPayload(error: Error, localContext = {}): Promise<Partial<NoticeTransportPayload>> {
|
|
67
|
-
const notice: Notice = error as Notice
|
|
68
|
-
notice.stack = generateStackTrace()
|
|
69
|
-
|
|
70
|
-
notice.backtrace = makeBacktrace(notice.stack)
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
notifier,
|
|
74
|
-
error: {
|
|
75
|
-
class: notice.name as string,
|
|
76
|
-
message: notice.message as string,
|
|
77
|
-
backtrace: notice.backtrace,
|
|
78
|
-
// fingerprint: this.calculateFingerprint(notice),
|
|
79
|
-
tags: notice.tags || [],
|
|
80
|
-
causes: getCauses(notice, this.logger),
|
|
81
|
-
},
|
|
82
|
-
request: {
|
|
83
|
-
url: notice.url,
|
|
84
|
-
component: notice.component,
|
|
85
|
-
action: notice.action,
|
|
86
|
-
context: localContext || {},
|
|
87
|
-
cgi_data: {},
|
|
88
|
-
params: {},
|
|
89
|
-
session: {},
|
|
90
|
-
},
|
|
91
|
-
server: {
|
|
92
|
-
project_root: notice.projectRoot,
|
|
93
|
-
environment_name: this.userAgent,
|
|
94
|
-
revision: version,
|
|
95
|
-
hostname: this.platform,
|
|
96
|
-
time: new Date().toUTCString(),
|
|
97
|
-
},
|
|
98
|
-
details: notice.details || {},
|
|
27
|
+
try {
|
|
28
|
+
// Set context for this error report
|
|
29
|
+
if (context) {
|
|
30
|
+
this.client.setContext(context)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Notify Honeybadger
|
|
34
|
+
await this.client.notify(error)
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error('ErrorReporter: Failed to send error report', err)
|
|
37
|
+
} finally {
|
|
38
|
+
// Clear context after reporting (always runs if context was set)
|
|
39
|
+
if (context) {
|
|
40
|
+
this.client.clear()
|
|
41
|
+
}
|
|
99
42
|
}
|
|
100
43
|
}
|
|
101
44
|
}
|
package/src/utils/index.ts
CHANGED