@quiltt/core 4.5.0 → 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/core
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
+ ## 4.5.1
14
+
15
+ ### Patch Changes
16
+
17
+ - [#389](https://github.com/quiltt/quiltt-js/pull/389) [`a6a2a7e`](https://github.com/quiltt/quiltt-js/commit/a6a2a7ea94c7204a69b53f191ee738bcdc520a10) Thanks [@zubairaziz](https://github.com/zubairaziz)! - Upgrade React versions across all projects
18
+
3
19
  ## 4.5.0
4
20
 
5
21
  ### Minor Changes
package/README.md CHANGED
@@ -5,26 +5,28 @@
5
5
 
6
6
  `@quiltt/core` provides essential primitives for building Javascript-based applications with Quiltt. It provides an Auth API client and modules for handling JSON Web Tokens (JWT), observables, storage management, timeouts, API handling, and Typescript types.
7
7
 
8
- This package is used by both `@quiltt/react` and `@quiltt/react-native`. If you bundle it separately, we recommend keeping versions in sync to avoid issues with mismatched dependencies.
8
+ This package is used by both [`@quiltt/react`](../react#readme) and [`@quiltt/react-native`](../react-native#readme). If you bundle it separately, we recommend keeping versions in sync to avoid issues with mismatched dependencies.
9
+
10
+ For general project information and contributing guidelines, see the [main repository README](../../README.md).
9
11
 
10
12
  ## Install
11
13
 
12
14
  With `npm`:
13
15
 
14
16
  ```shell
15
- $ npm install @quiltt/core
17
+ npm install @quiltt/core
16
18
  ```
17
19
 
18
20
  With `yarn`:
19
21
 
20
22
  ```shell
21
- $ yarn add @quiltt/core
23
+ yarn add @quiltt/core
22
24
  ```
23
25
 
24
26
  With `pnpm`:
25
27
 
26
28
  ```shell
27
- $ pnpm add @quiltt/core
29
+ pnpm add @quiltt/core
28
30
  ```
29
31
 
30
32
  ## Auth API Client
@@ -39,10 +41,10 @@ import { AuthAPI } from '@quiltt/core'
39
41
  const auth = new AuthAPI('{CONNECTOR_ID}')
40
42
 
41
43
  // Check if a Session token is valid
42
- auth.ping('{SESSION_TOKEN}')
44
+ await auth.ping('{SESSION_TOKEN}')
43
45
 
44
46
  // Revoke a Session token
45
- auth.revoke('{SESSION_TOKEN}')
47
+ await auth.revoke('{SESSION_TOKEN}')
46
48
  ```
47
49
 
48
50
  ## Modules
@@ -74,7 +76,15 @@ The `types` module provides a collection of TypeScript type definitions and inte
74
76
  ## Usage
75
77
 
76
78
  ```javascript
77
- import { JsonWebToken, Observable, Storage, Timeoutable, api, types } from '@quiltt/core'
79
+ import {
80
+ AuthAPI,
81
+ JsonWebToken,
82
+ Observable,
83
+ Storage,
84
+ Timeoutable,
85
+ ConnectorSDK,
86
+ ConnectorSDKEventType
87
+ } from '@quiltt/core'
78
88
 
79
89
  // Example usage of the library modules
80
90
  // ...
@@ -90,4 +100,9 @@ This project is licensed under the terms of the MIT license. See the [LICENSE](L
90
100
 
91
101
  ## Contributing
92
102
 
93
- For information on how to contribute to this project, please refer to the [CONTRIBUTING.md](CONTRIBUTING.md) file.
103
+ For information on how to contribute to this project, please refer to the [repository contributing guidelines](../../CONTRIBUTING.md).
104
+
105
+ ## Related Packages
106
+
107
+ - [`@quiltt/react`](../react#readme) - React components and hooks
108
+ - [`@quiltt/react-native`](../react-native#readme) - React Native and Expo components
@@ -1,11 +1,11 @@
1
1
  'use client';
2
- import { ApolloLink } from '@apollo/client/core/index.js';
3
- import { Observable as Observable$1 } from '@apollo/client/utilities/index.js';
2
+ import { ApolloLink } from '@apollo/client/core';
4
3
  import { createConsumer } from '@rails/actioncable';
5
4
  import { print } from 'graphql';
5
+ import { Observable as Observable$1 } from 'rxjs';
6
6
 
7
7
  var name = "@quiltt/core";
8
- var version$1 = "4.5.0";
8
+ var version$1 = "5.0.0";
9
9
 
10
10
  const QUILTT_API_INSECURE = (()=>{
11
11
  try {
@@ -330,6 +330,7 @@ const endpointWebsockets = `${protocolWebsockets}://api.${domain}/websockets`;
330
330
  * basically acts like shared memory when there is no localStorage.
331
331
  */ const GlobalStorage = new Storage();
332
332
 
333
+ // Adapted from https://github.com/rmosolgo/graphql-ruby/blob/master/javascript_client/src/subscriptions/ActionCableLink.ts
333
334
  class ActionCableLink extends ApolloLink {
334
335
  constructor(options){
335
336
  super();
@@ -337,6 +338,7 @@ class ActionCableLink extends ApolloLink {
337
338
  this.channelName = options.channelName || 'GraphqlChannel';
338
339
  this.actionName = options.actionName || 'execute';
339
340
  this.connectionParams = options.connectionParams || {};
341
+ this.callbacks = options.callbacks || {};
340
342
  }
341
343
  // Interestingly, this link does _not_ call through to `next` because
342
344
  // instead, it sends the request to ActionCable.
@@ -344,7 +346,9 @@ class ActionCableLink extends ApolloLink {
344
346
  const token = GlobalStorage.get('session');
345
347
  if (!token) {
346
348
  console.warn('QuilttClient attempted to send an unauthenticated Subscription');
347
- return null;
349
+ return new Observable$1((observer)=>{
350
+ observer.error(new Error('No authentication token available'));
351
+ });
348
352
  }
349
353
  if (!this.cables[token]) {
350
354
  this.cables[token] = createConsumer(endpointWebsockets + (token ? `?token=${token}` : ''));
@@ -353,11 +357,12 @@ class ActionCableLink extends ApolloLink {
353
357
  const channelId = Math.round(Date.now() + Math.random() * 100000).toString(16);
354
358
  const actionName = this.actionName;
355
359
  const connectionParams = typeof this.connectionParams === 'function' ? this.connectionParams(operation) : this.connectionParams;
360
+ const callbacks = this.callbacks;
356
361
  const channel = this.cables[token].subscriptions.create(Object.assign({}, {
357
362
  channel: this.channelName,
358
363
  channelId: channelId
359
364
  }, connectionParams), {
360
- connected: ()=>{
365
+ connected: (args)=>{
361
366
  channel.perform(actionName, {
362
367
  query: operation.query ? print(operation.query) : null,
363
368
  variables: operation.variables,
@@ -365,6 +370,7 @@ class ActionCableLink extends ApolloLink {
365
370
  operationId: operation.operationId,
366
371
  operationName: operation.operationName
367
372
  });
373
+ callbacks.connected?.(args);
368
374
  },
369
375
  received: (payload)=>{
370
376
  if (payload?.result?.data || payload?.result?.errors) {
@@ -373,6 +379,10 @@ class ActionCableLink extends ApolloLink {
373
379
  if (!payload.more) {
374
380
  observer.complete();
375
381
  }
382
+ callbacks.received?.(payload);
383
+ },
384
+ disconnected: ()=>{
385
+ callbacks.disconnected?.();
376
386
  }
377
387
  });
378
388
  // Make the ActionCable subscription behave like an Apollo subscription
@@ -391,4 +401,4 @@ class SubscriptionLink extends ActionCableLink {
391
401
  }
392
402
  }
393
403
 
394
- export { GlobalStorage as G, LocalStorage as L, MemoryStorage as M, Observable as O, SubscriptionLink as S, endpointAuth as a, endpointRest as b, cdnBase as c, debugging as d, endpointGraphQL as e, endpointWebsockets as f, Storage as g, version as v };
404
+ export { GlobalStorage as G, LocalStorage as L, MemoryStorage as M, Observable as O, SubscriptionLink as S, endpointAuth as a, endpointRest as b, Storage as c, debugging as d, endpointGraphQL as e, cdnBase as f, endpointWebsockets as g, version as v };
package/dist/index.d.ts CHANGED
@@ -1,18 +1,14 @@
1
- import * as _apollo_client_core from '@apollo/client/core';
2
- import { ApolloClientOptions, NormalizedCacheObject, Operation, NextLink, FetchResult } from '@apollo/client/core';
3
- export { ApolloError, OperationVariables } from '@apollo/client/core';
4
- import { ApolloLink, ApolloClient } from '@apollo/client/core/index.js';
5
- export { gql } from '@apollo/client/core/index.js';
6
- import { Observable as Observable$1 } from '@apollo/client/utilities';
7
- import { BatchHttpLink as BatchHttpLink$1 } from '@apollo/client/link/batch-http/index.js';
8
- import { HttpLink as HttpLink$1 } from '@apollo/client/link/http/index.js';
9
- import { RetryLink as RetryLink$1 } from '@apollo/client/link/retry/index.js';
10
- import { Observable as Observable$2 } from '@apollo/client/utilities/index.js';
1
+ import { ApolloClient, ApolloLink } from '@apollo/client/core';
2
+ export { OperationVariables, gql } from '@apollo/client/core';
3
+ import { Observable as Observable$1 } from 'rxjs';
4
+ import { BatchHttpLink as BatchHttpLink$1 } from '@apollo/client/link/batch-http';
5
+ import { ErrorLink as ErrorLink$1 } from '@apollo/client/link/error';
6
+ import { HttpLink as HttpLink$1 } from '@apollo/client/link/http';
7
+ import { RetryLink as RetryLink$1 } from '@apollo/client/link/retry';
11
8
  import { Consumer } from '@rails/actioncable';
12
9
  import { Dispatch, SetStateAction } from 'react';
13
- export { NormalizedCacheObject } from '@apollo/client/cache';
14
- export { InMemoryCache } from '@apollo/client/cache/index.js';
15
- export { useMutation, useQuery, useSubscription } from '@apollo/client/react/hooks/index.js';
10
+ export { InMemoryCache, NormalizedCacheObject } from '@apollo/client/cache';
11
+ export { useMutation, useQuery, useSubscription } from '@apollo/client/react';
16
12
 
17
13
  interface CallbackManager {
18
14
  onEvent(callback: ConnectorSDKOnEventCallback): void;
@@ -140,12 +136,14 @@ type ConnectorSDKConnectorOptions = ConnectorSDKCallbacks & {
140
136
  nonce?: string;
141
137
  };
142
138
 
143
- type QuilttClientOptions<T> = Omit<ApolloClientOptions<T>, 'link'> & {
139
+ type QuilttClientOptions = Omit<ApolloClient.Options, 'link'> & {
144
140
  /** An array of initial links to inject before the default Quiltt Links */
145
141
  customLinks?: ApolloLink[];
142
+ /** Platform-specific version link (required) */
143
+ versionLink: ApolloLink;
146
144
  };
147
- declare class QuilttClient extends ApolloClient<NormalizedCacheObject> {
148
- constructor(options: QuilttClientOptions<NormalizedCacheObject>);
145
+ declare class QuilttClient extends ApolloClient {
146
+ constructor(options: QuilttClientOptions);
149
147
  }
150
148
 
151
149
  /**
@@ -155,12 +153,12 @@ declare class QuilttClient extends ApolloClient<NormalizedCacheObject> {
155
153
  * valid sessions during rotation and networking weirdness.
156
154
  */
157
155
  declare class AuthLink extends ApolloLink {
158
- request(operation: Operation, forward: NextLink): Observable$1<FetchResult> | null;
156
+ request(operation: ApolloLink.Operation, forward: ApolloLink.ForwardFunction): Observable$1<ApolloLink.Result>;
159
157
  }
160
158
 
161
159
  declare const BatchHttpLink: BatchHttpLink$1;
162
160
 
163
- declare const ErrorLink: _apollo_client_core.ApolloLink;
161
+ declare const ErrorLink: ErrorLink$1;
164
162
 
165
163
  declare const ForwardableLink: ApolloLink;
166
164
 
@@ -168,10 +166,17 @@ declare const HttpLink: HttpLink$1;
168
166
 
169
167
  declare const RetryLink: RetryLink$1;
170
168
 
171
- type RequestResult = FetchResult<{
169
+ type RequestResult = ApolloLink.Result<{
172
170
  [key: string]: unknown;
173
- }, Record<string, unknown>, Record<string, unknown>>;
174
- type ConnectionParams = object | ((operation: Operation) => object);
171
+ }>;
172
+ type ConnectionParams = object | ((operation: ApolloLink.Operation) => object);
173
+ type SubscriptionCallbacks = {
174
+ connected?: (args?: {
175
+ reconnected: boolean;
176
+ }) => void;
177
+ disconnected?: () => void;
178
+ received?: (payload: unknown) => void;
179
+ };
175
180
  declare class ActionCableLink extends ApolloLink {
176
181
  cables: {
177
182
  [id: string]: Consumer;
@@ -179,12 +184,14 @@ declare class ActionCableLink extends ApolloLink {
179
184
  channelName: string;
180
185
  actionName: string;
181
186
  connectionParams: ConnectionParams;
187
+ callbacks: SubscriptionCallbacks;
182
188
  constructor(options: {
183
189
  channelName?: string;
184
190
  actionName?: string;
185
191
  connectionParams?: ConnectionParams;
192
+ callbacks?: SubscriptionCallbacks;
186
193
  });
187
- request(operation: Operation, _next: NextLink): Observable$2<RequestResult> | null;
194
+ request(operation: ApolloLink.Operation, _next: ApolloLink.ForwardFunction): Observable$1<RequestResult>;
188
195
  }
189
196
 
190
197
  declare class SubscriptionLink extends ActionCableLink {
@@ -193,7 +200,7 @@ declare class SubscriptionLink extends ActionCableLink {
193
200
 
194
201
  declare const TerminatingLink: ApolloLink;
195
202
 
196
- declare const VersionLink: ApolloLink;
203
+ declare const createVersionLink: (platformInfo: string) => ApolloLink;
197
204
 
198
205
  type FetchResponse<T> = {
199
206
  data: T;
@@ -292,8 +299,8 @@ type SearchResponse = FetchResponse<InstitutionsData>;
292
299
  type ResolvableResponse = FetchResponse<ResolvableData>;
293
300
  declare class ConnectorsAPI {
294
301
  clientId: string;
295
- agent: string;
296
- constructor(clientId: string, agent?: string);
302
+ userAgent: string;
303
+ constructor(clientId: string, userAgent?: string);
297
304
  /**
298
305
  * Response Statuses:
299
306
  * - 200: OK -> Institutions Found
@@ -496,5 +503,22 @@ declare class Timeoutable {
496
503
  private broadcast;
497
504
  }
498
505
 
499
- export { AuthAPI, AuthLink, AuthStrategies, BatchHttpLink, ConnectorSDKEventType, ConnectorsAPI, ErrorLink, ForwardableLink, GlobalStorage, HttpLink, JsonWebTokenParse, LocalStorage, MemoryStorage, Observable, QuilttClient, RetryLink, Storage, SubscriptionLink, TerminatingLink, Timeoutable, VersionLink, cdnBase, debugging, endpointAuth, endpointGraphQL, endpointRest, endpointWebsockets, version };
506
+ /**
507
+ * Extracts version number from formatted version string
508
+ * @param formattedVersion - Formatted version like "@quiltt/core: v4.5.1"
509
+ * @returns Version number like "4.5.1" or "unknown" if not found
510
+ */
511
+ declare const extractVersionNumber: (formattedVersion: string) => string;
512
+ /**
513
+ * Generates a User-Agent string following standard format
514
+ * Format: Quiltt/<version> (<platform-info>)
515
+ */
516
+ declare const getUserAgent: (sdkVersion: string, platformInfo: string) => string;
517
+ /**
518
+ * Detects browser information from user agent string
519
+ * Returns browser name and version, or 'Unknown' if not detected
520
+ */
521
+ declare const getBrowserInfo: () => string;
522
+
523
+ export { AuthAPI, AuthLink, AuthStrategies, BatchHttpLink, ConnectorSDKEventType, ConnectorsAPI, ErrorLink, ForwardableLink, GlobalStorage, HttpLink, JsonWebTokenParse, LocalStorage, MemoryStorage, Observable, QuilttClient, RetryLink, Storage, SubscriptionLink, TerminatingLink, Timeoutable, cdnBase, createVersionLink, debugging, endpointAuth, endpointGraphQL, endpointRest, endpointWebsockets, extractVersionNumber, getBrowserInfo, getUserAgent, version };
500
524
  export type { BadRequestResponse, Claims, ConnectorSDK, ConnectorSDKCallbackMetadata, ConnectorSDKCallbacks, ConnectorSDKConnectOptions, ConnectorSDKConnector, ConnectorSDKConnectorOptions, ConnectorSDKOnEventCallback, ConnectorSDKOnEventExitCallback, ConnectorSDKOnExitAbortCallback, ConnectorSDKOnExitErrorCallback, ConnectorSDKOnExitSuccessCallback, ConnectorSDKOnLoadCallback, ConnectorSDKOnOpenCallback, ConnectorSDKReconnectOptions, DeepPartial, DeepReadonly, ErrorData, Exact, InputMaybe, InstitutionData, InstitutionsData, JsonWebToken, MakeMaybe, MakeOptional, Maybe, Mutable, NoContentData, Nullable, Observer, PasscodePayload, PrivateClaims, QuilttClientOptions, QuilttJWT, RegisteredClaims, ResolvableData, ResolvableResponse, SearchResponse, SessionResponse, UnauthorizedData, UnauthorizedResponse, UnprocessableData, UnprocessableResponse, UsernamePayload };
package/dist/index.js CHANGED
@@ -1,14 +1,15 @@
1
- import { ApolloLink, ApolloClient } from '@apollo/client/core/index.js';
2
- export { gql } from '@apollo/client/core/index.js';
3
- import { G as GlobalStorage, e as endpointGraphQL, v as version, d as debugging, S as SubscriptionLink, a as endpointAuth, b as endpointRest } from './SubscriptionLink-12s-2Lc2oaGT.js';
4
- export { L as LocalStorage, M as MemoryStorage, O as Observable, g as Storage, c as cdnBase, f as endpointWebsockets } from './SubscriptionLink-12s-2Lc2oaGT.js';
5
- import { BatchHttpLink as BatchHttpLink$1 } from '@apollo/client/link/batch-http/index.js';
1
+ import { ApolloLink, ApolloClient } from '@apollo/client/core';
2
+ export { gql } from '@apollo/client/core';
3
+ import { G as GlobalStorage, e as endpointGraphQL, v as version, d as debugging, S as SubscriptionLink, a as endpointAuth, b as endpointRest } from './SubscriptionLink-12s-C2VbF8Tf.js';
4
+ export { L as LocalStorage, M as MemoryStorage, O as Observable, c as Storage, f as cdnBase, g as endpointWebsockets } from './SubscriptionLink-12s-C2VbF8Tf.js';
5
+ import { Observable } from 'rxjs';
6
+ import { BatchHttpLink as BatchHttpLink$1 } from '@apollo/client/link/batch-http';
6
7
  import crossfetch from 'cross-fetch';
7
- import { onError } from '@apollo/client/link/error/index.js';
8
- import { HttpLink as HttpLink$1 } from '@apollo/client/link/http/index.js';
9
- import { RetryLink as RetryLink$1 } from '@apollo/client/link/retry/index.js';
10
- export { InMemoryCache } from '@apollo/client/cache/index.js';
11
- export { useMutation, useQuery, useSubscription } from '@apollo/client/react/hooks/index.js';
8
+ import { ErrorLink as ErrorLink$1 } from '@apollo/client/link/error';
9
+ import { HttpLink as HttpLink$1 } from '@apollo/client/link/http';
10
+ import { RetryLink as RetryLink$1 } from '@apollo/client/link/retry';
11
+ export { InMemoryCache } from '@apollo/client/cache';
12
+ export { useMutation, useQuery, useSubscription } from '@apollo/client/react';
12
13
 
13
14
  /**
14
15
  * Enum representing the different types of events emitted by the Connector.
@@ -31,7 +32,9 @@ export { useMutation, useQuery, useSubscription } from '@apollo/client/react/hoo
31
32
  const token = GlobalStorage.get('session');
32
33
  if (!token) {
33
34
  console.warn('QuilttLink attempted to send an unauthenticated Query');
34
- return null;
35
+ return new Observable((observer)=>{
36
+ observer.error(new Error('No authentication token available'));
37
+ });
35
38
  }
36
39
  operation.setContext(({ headers = {} })=>({
37
40
  headers: {
@@ -50,9 +53,12 @@ const BatchHttpLink = new BatchHttpLink$1({
50
53
  fetch: effectiveFetch$2
51
54
  });
52
55
 
53
- const ErrorLink = onError(({ graphQLErrors, networkError })=>{
54
- if (graphQLErrors) {
55
- graphQLErrors.forEach(({ message, path, extensions })=>{
56
+ const ErrorLink = new ErrorLink$1(({ error, result })=>{
57
+ // In Apollo Client 4, errors are consolidated to the 'error' and 'result' properties
58
+ // Handle GraphQL errors from result
59
+ if (result?.errors) {
60
+ result.errors.forEach((graphQLError)=>{
61
+ const { message, path, extensions } = graphQLError;
56
62
  const formattedPath = Array.isArray(path) ? path.join('.') : path ?? 'N/A';
57
63
  const parts = [
58
64
  `[Quiltt][GraphQL Error]: ${message}`,
@@ -65,12 +71,15 @@ const ErrorLink = onError(({ graphQLErrors, networkError })=>{
65
71
  console.warn(parts.join(' | '));
66
72
  });
67
73
  }
68
- if (networkError) {
69
- if (networkError.statusCode === 401) {
70
- console.warn('[Quiltt][Authentication Error]:', networkError);
74
+ // Handle network/server errors
75
+ if (error) {
76
+ if ('statusCode' in error && error.statusCode === 401) {
77
+ console.warn('[Quiltt][Authentication Error]:', error);
71
78
  GlobalStorage.set('session', null);
79
+ } else if ('statusCode' in error) {
80
+ console.warn('[Quiltt][Server Error]:', error);
72
81
  } else {
73
- console.warn('[Quiltt][Network Error]:', networkError);
82
+ console.warn('[Quiltt][Network Error]:', error);
74
83
  }
75
84
  }
76
85
  });
@@ -86,22 +95,91 @@ const HttpLink = new HttpLink$1({
86
95
 
87
96
  const RetryLink = new RetryLink$1({
88
97
  attempts: {
89
- retryIf: (error, _operation)=>!!error && (!error.statusCode || error.statusCode >= 500)
98
+ retryIf: (error, _operation)=>{
99
+ if (!error) return false;
100
+ const statusCode = 'statusCode' in error ? error.statusCode : undefined;
101
+ return !statusCode || statusCode >= 500;
102
+ }
90
103
  }
91
104
  });
92
105
 
93
- const TerminatingLink = new ApolloLink(()=>null);
94
-
95
- const VersionLink = new ApolloLink((operation, forward)=>{
96
- operation.setContext(({ headers = {} })=>({
97
- headers: {
98
- ...headers,
99
- 'Quiltt-Client-Version': version
100
- }
101
- }));
102
- return forward(operation);
106
+ const TerminatingLink = new ApolloLink(()=>{
107
+ return new Observable((observer)=>{
108
+ observer.complete();
109
+ });
103
110
  });
104
111
 
112
+ /**
113
+ * Extracts version number from formatted version string
114
+ * @param formattedVersion - Formatted version like "@quiltt/core: v4.5.1"
115
+ * @returns Version number like "4.5.1" or "unknown" if not found
116
+ */ const extractVersionNumber = (formattedVersion)=>{
117
+ // Find the 'v' prefix and extract version after it
118
+ const vIndex = formattedVersion.indexOf('v');
119
+ if (vIndex === -1) return 'unknown';
120
+ const versionPart = formattedVersion.substring(vIndex + 1);
121
+ const parts = versionPart.split('.');
122
+ // Validate we have at least major.minor.patch
123
+ if (parts.length < 3) return 'unknown';
124
+ // Extract numeric parts (handles cases like "4.5.1-beta")
125
+ const major = parts[0].match(/^\d+/)?.[0];
126
+ const minor = parts[1].match(/^\d+/)?.[0];
127
+ const patch = parts[2].match(/^\d+/)?.[0];
128
+ if (!major || !minor || !patch) return 'unknown';
129
+ return `${major}.${minor}.${patch}`;
130
+ };
131
+ /**
132
+ * Generates a User-Agent string following standard format
133
+ * Format: Quiltt/<version> (<platform-info>)
134
+ */ const getUserAgent = (sdkVersion, platformInfo)=>{
135
+ return `Quiltt/${sdkVersion} (${platformInfo})`;
136
+ };
137
+ /**
138
+ * Detects browser information from user agent string
139
+ * Returns browser name and version, or 'Unknown' if not detected
140
+ */ const getBrowserInfo = ()=>{
141
+ if (typeof navigator === 'undefined' || !navigator.userAgent) {
142
+ return 'Unknown';
143
+ }
144
+ const ua = navigator.userAgent;
145
+ // Edge (must be checked before Chrome)
146
+ if (ua.includes('Edg/')) {
147
+ const version = ua.match(/Edg\/(\d+)/)?.[1] || 'Unknown';
148
+ return `Edge/${version}`;
149
+ }
150
+ // Chrome
151
+ if (ua.includes('Chrome/') && !ua.includes('Edg/')) {
152
+ const version = ua.match(/Chrome\/(\d+)/)?.[1] || 'Unknown';
153
+ return `Chrome/${version}`;
154
+ }
155
+ // Safari (must be checked after Chrome)
156
+ if (ua.includes('Safari/') && !ua.includes('Chrome/')) {
157
+ const version = ua.match(/Version\/(\d+)/)?.[1] || 'Unknown';
158
+ return `Safari/${version}`;
159
+ }
160
+ // Firefox
161
+ if (ua.includes('Firefox/')) {
162
+ const version = ua.match(/Firefox\/(\d+)/)?.[1] || 'Unknown';
163
+ return `Firefox/${version}`;
164
+ }
165
+ return 'Unknown';
166
+ };
167
+
168
+ const createVersionLink = (platformInfo)=>{
169
+ const versionNumber = extractVersionNumber(version);
170
+ const userAgent = getUserAgent(versionNumber, platformInfo);
171
+ return new ApolloLink((operation, forward)=>{
172
+ operation.setContext(({ headers = {} })=>({
173
+ headers: {
174
+ ...headers,
175
+ 'Quiltt-Client-Version': version,
176
+ 'User-Agent': userAgent
177
+ }
178
+ }));
179
+ return forward(operation);
180
+ });
181
+ };
182
+
105
183
  class QuilttClient extends ApolloClient {
106
184
  constructor(options){
107
185
  const finalOptions = {
@@ -124,7 +202,7 @@ class QuilttClient extends ApolloClient {
124
202
  const subscriptionsLink = new SubscriptionLink();
125
203
  const quilttLink = ApolloLink.from([
126
204
  ...initialLinks,
127
- VersionLink,
205
+ options.versionLink,
128
206
  authLink,
129
207
  ErrorLink,
130
208
  RetryLink
@@ -268,7 +346,7 @@ class AuthAPI {
268
346
  }
269
347
 
270
348
  class ConnectorsAPI {
271
- constructor(clientId, agent = 'web'){
349
+ constructor(clientId, userAgent = getUserAgent(extractVersionNumber(version), 'Unknown')){
272
350
  /**
273
351
  * Response Statuses:
274
352
  * - 200: OK -> Institutions Found
@@ -309,7 +387,7 @@ class ConnectorsAPI {
309
387
  const headers = new Headers();
310
388
  headers.set('Content-Type', 'application/json');
311
389
  headers.set('Accept', 'application/json');
312
- headers.set('Quiltt-SDK-Agent', this.agent);
390
+ headers.set('User-Agent', this.userAgent);
313
391
  headers.set('Authorization', `Bearer ${token}`);
314
392
  return {
315
393
  headers,
@@ -319,7 +397,7 @@ class ConnectorsAPI {
319
397
  };
320
398
  this.validateStatus = (status)=>status < 500 && status !== 429;
321
399
  this.clientId = clientId;
322
- this.agent = agent;
400
+ this.userAgent = userAgent;
323
401
  }
324
402
  }
325
403
 
@@ -368,4 +446,4 @@ const JsonWebTokenParse = (token)=>{
368
446
  }
369
447
  }
370
448
 
371
- export { AuthAPI, AuthLink, AuthStrategies, BatchHttpLink, ConnectorSDKEventType, ConnectorsAPI, ErrorLink, ForwardableLink, GlobalStorage, HttpLink, JsonWebTokenParse, QuilttClient, RetryLink, SubscriptionLink, TerminatingLink, Timeoutable, VersionLink, debugging, endpointAuth, endpointGraphQL, endpointRest, version };
449
+ export { AuthAPI, AuthLink, AuthStrategies, BatchHttpLink, ConnectorSDKEventType, ConnectorsAPI, ErrorLink, ForwardableLink, GlobalStorage, HttpLink, JsonWebTokenParse, QuilttClient, RetryLink, SubscriptionLink, TerminatingLink, Timeoutable, createVersionLink, debugging, endpointAuth, endpointGraphQL, endpointRest, extractVersionNumber, getBrowserInfo, getUserAgent, version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quiltt/core",
3
- "version": "4.5.0",
3
+ "version": "5.0.0",
4
4
  "description": "Javascript API client and utilities for Quiltt",
5
5
  "keywords": [
6
6
  "quiltt",
@@ -32,21 +32,21 @@
32
32
  ],
33
33
  "main": "dist/index.js",
34
34
  "dependencies": {
35
- "@apollo/client": "^3.14.0",
36
- "@rails/actioncable": "^8.0.300",
35
+ "@apollo/client": "^4.1.3",
36
+ "@rails/actioncable": "^8.1.200",
37
37
  "braces": "^3.0.3",
38
- "cross-fetch": "^4.0.0",
39
- "graphql": "^16.10.0",
40
- "graphql-ruby-client": "^1.14.5"
38
+ "cross-fetch": "^4.1.0",
39
+ "graphql": "^16.12.0",
40
+ "rxjs": "^7.8.2"
41
41
  },
42
42
  "devDependencies": {
43
- "@biomejs/biome": "2.2.4",
44
- "@types/node": "22.18.6",
45
- "@types/rails__actioncable": "6.1.11",
46
- "@types/react": "18.3.23",
47
- "bunchee": "6.6.0",
48
- "rimraf": "6.0.1",
49
- "typescript": "5.9.2"
43
+ "@biomejs/biome": "2.3.13",
44
+ "@types/node": "24.10.9",
45
+ "@types/rails__actioncable": "8.0.3",
46
+ "@types/react": "19.2.10",
47
+ "bunchee": "6.9.4",
48
+ "rimraf": "6.1.2",
49
+ "typescript": "5.9.3"
50
50
  },
51
51
  "tags": [
52
52
  "quiltt"
@@ -1,5 +1,4 @@
1
- import type { ApolloClientOptions, NormalizedCacheObject, Operation } from '@apollo/client/core'
2
- import { ApolloClient, ApolloLink } from '@apollo/client/core/index.js'
1
+ import { ApolloClient, ApolloLink } from '@apollo/client/core'
3
2
  import type { DefinitionNode, OperationDefinitionNode } from 'graphql'
4
3
 
5
4
  import { debugging } from '@/configuration'
@@ -12,16 +11,17 @@ import {
12
11
  HttpLink,
13
12
  RetryLink,
14
13
  SubscriptionLink,
15
- VersionLink,
16
14
  } from './links'
17
15
 
18
- export type QuilttClientOptions<T> = Omit<ApolloClientOptions<T>, 'link'> & {
16
+ export type QuilttClientOptions = Omit<ApolloClient.Options, 'link'> & {
19
17
  /** An array of initial links to inject before the default Quiltt Links */
20
18
  customLinks?: ApolloLink[]
19
+ /** Platform-specific version link (required) */
20
+ versionLink: ApolloLink
21
21
  }
22
22
 
23
- export class QuilttClient extends ApolloClient<NormalizedCacheObject> {
24
- constructor(options: QuilttClientOptions<NormalizedCacheObject>) {
23
+ export class QuilttClient extends ApolloClient {
24
+ constructor(options: QuilttClientOptions) {
25
25
  const finalOptions = {
26
26
  ...options,
27
27
  devtools: {
@@ -34,13 +34,13 @@ export class QuilttClient extends ApolloClient<NormalizedCacheObject> {
34
34
  const isOperationDefinition = (def: DefinitionNode): def is OperationDefinitionNode =>
35
35
  def.kind === 'OperationDefinition'
36
36
 
37
- const isSubscriptionOperation = (operation: Operation) => {
37
+ const isSubscriptionOperation = (operation: ApolloLink.Operation) => {
38
38
  return operation.query.definitions.some(
39
39
  (definition) => isOperationDefinition(definition) && definition.operation === 'subscription'
40
40
  )
41
41
  }
42
42
 
43
- const isBatchable = (operation: Operation) => {
43
+ const isBatchable = (operation: ApolloLink.Operation) => {
44
44
  return operation.getContext().batchable ?? true
45
45
  }
46
46
 
@@ -49,12 +49,12 @@ export class QuilttClient extends ApolloClient<NormalizedCacheObject> {
49
49
 
50
50
  const quilttLink = ApolloLink.from([
51
51
  ...initialLinks,
52
- VersionLink,
52
+ options.versionLink,
53
53
  authLink,
54
54
  ErrorLink,
55
55
  RetryLink,
56
- ])
57
- .split(isSubscriptionOperation, subscriptionsLink, ForwardableLink)
56
+ ] as ApolloLink[])
57
+ .split(isSubscriptionOperation, subscriptionsLink as ApolloLink, ForwardableLink)
58
58
  .split(isBatchable, BatchHttpLink, HttpLink)
59
59
 
60
60
  super({
@@ -71,8 +71,8 @@ export class QuilttClient extends ApolloClient<NormalizedCacheObject> {
71
71
 
72
72
  /** Client and Tooling */
73
73
  export type { NormalizedCacheObject } from '@apollo/client/cache'
74
- export { InMemoryCache } from '@apollo/client/cache/index.js'
75
- export type { ApolloError, OperationVariables } from '@apollo/client/core'
76
- export { gql } from '@apollo/client/core/index.js'
74
+ export { InMemoryCache } from '@apollo/client/cache'
75
+ export type { OperationVariables } from '@apollo/client/core'
76
+ export { gql } from '@apollo/client/core'
77
77
  /** React hooks used by @quiltt/react-native and @quiltt/react */
78
- export { useMutation, useQuery, useSubscription } from '@apollo/client/react/hooks/index.js'
78
+ export { useMutation, useQuery, useSubscription } from '@apollo/client/react'
@@ -1,46 +1,55 @@
1
- import type { FetchResult, NextLink, Operation } from '@apollo/client/core'
2
- import { ApolloLink } from '@apollo/client/core/index.js'
3
- import { Observable } from '@apollo/client/utilities/index.js'
1
+ // Adapted from https://github.com/rmosolgo/graphql-ruby/blob/master/javascript_client/src/subscriptions/ActionCableLink.ts
2
+ import { ApolloLink } from '@apollo/client/core'
4
3
  import type { Consumer } from '@rails/actioncable'
5
4
  import { createConsumer } from '@rails/actioncable'
6
5
  import { print } from 'graphql'
6
+ import { Observable } from 'rxjs'
7
7
 
8
8
  import { endpointWebsockets } from '@/configuration'
9
9
  import { GlobalStorage } from '@/storage'
10
10
 
11
- type RequestResult = FetchResult<
12
- { [key: string]: unknown },
13
- Record<string, unknown>,
14
- Record<string, unknown>
15
- >
16
- type ConnectionParams = object | ((operation: Operation) => object)
11
+ type RequestResult = ApolloLink.Result<{ [key: string]: unknown }>
12
+ type ConnectionParams = object | ((operation: ApolloLink.Operation) => object)
13
+ type SubscriptionCallbacks = {
14
+ connected?: (args?: { reconnected: boolean }) => void
15
+ disconnected?: () => void
16
+ received?: (payload: unknown) => void
17
+ }
17
18
 
18
19
  class ActionCableLink extends ApolloLink {
19
20
  cables: { [id: string]: Consumer }
20
21
  channelName: string
21
22
  actionName: string
22
23
  connectionParams: ConnectionParams
24
+ callbacks: SubscriptionCallbacks
23
25
 
24
26
  constructor(options: {
25
27
  channelName?: string
26
28
  actionName?: string
27
29
  connectionParams?: ConnectionParams
30
+ callbacks?: SubscriptionCallbacks
28
31
  }) {
29
32
  super()
30
33
  this.cables = {}
31
34
  this.channelName = options.channelName || 'GraphqlChannel'
32
35
  this.actionName = options.actionName || 'execute'
33
36
  this.connectionParams = options.connectionParams || {}
37
+ this.callbacks = options.callbacks || {}
34
38
  }
35
39
 
36
40
  // Interestingly, this link does _not_ call through to `next` because
37
41
  // instead, it sends the request to ActionCable.
38
- request(operation: Operation, _next: NextLink): Observable<RequestResult> | null {
42
+ request(
43
+ operation: ApolloLink.Operation,
44
+ _next: ApolloLink.ForwardFunction
45
+ ): Observable<RequestResult> {
39
46
  const token = GlobalStorage.get('session')
40
47
 
41
48
  if (!token) {
42
49
  console.warn('QuilttClient attempted to send an unauthenticated Subscription')
43
- return null
50
+ return new Observable((observer) => {
51
+ observer.error(new Error('No authentication token available'))
52
+ })
44
53
  }
45
54
 
46
55
  if (!this.cables[token]) {
@@ -55,6 +64,7 @@ class ActionCableLink extends ApolloLink {
55
64
  ? this.connectionParams(operation)
56
65
  : this.connectionParams
57
66
 
67
+ const callbacks = this.callbacks
58
68
  const channel = this.cables[token].subscriptions.create(
59
69
  Object.assign(
60
70
  {},
@@ -65,7 +75,7 @@ class ActionCableLink extends ApolloLink {
65
75
  connectionParams
66
76
  ),
67
77
  {
68
- connected: () => {
78
+ connected: (args?: { reconnected: boolean }) => {
69
79
  channel.perform(actionName, {
70
80
  query: operation.query ? print(operation.query) : null,
71
81
  variables: operation.variables,
@@ -73,6 +83,7 @@ class ActionCableLink extends ApolloLink {
73
83
  operationId: (operation as { operationId?: string }).operationId,
74
84
  operationName: operation.operationName,
75
85
  })
86
+ callbacks.connected?.(args)
76
87
  },
77
88
 
78
89
  received: (payload: { result: RequestResult; more: any }) => {
@@ -83,6 +94,11 @@ class ActionCableLink extends ApolloLink {
83
94
  if (!payload.more) {
84
95
  observer.complete()
85
96
  }
97
+
98
+ callbacks.received?.(payload)
99
+ },
100
+ disconnected: () => {
101
+ callbacks.disconnected?.()
86
102
  },
87
103
  }
88
104
  )
@@ -1,6 +1,5 @@
1
- import type { FetchResult, NextLink, Operation } from '@apollo/client/core'
2
- import { ApolloLink } from '@apollo/client/core/index.js'
3
- import type { Observable } from '@apollo/client/utilities'
1
+ import { ApolloLink } from '@apollo/client/core'
2
+ import { Observable } from 'rxjs'
4
3
 
5
4
  import { GlobalStorage } from '@/storage'
6
5
 
@@ -11,12 +10,17 @@ import { GlobalStorage } from '@/storage'
11
10
  * valid sessions during rotation and networking weirdness.
12
11
  */
13
12
  export class AuthLink extends ApolloLink {
14
- request(operation: Operation, forward: NextLink): Observable<FetchResult> | null {
13
+ request(
14
+ operation: ApolloLink.Operation,
15
+ forward: ApolloLink.ForwardFunction
16
+ ): Observable<ApolloLink.Result> {
15
17
  const token = GlobalStorage.get('session')
16
18
 
17
19
  if (!token) {
18
20
  console.warn('QuilttLink attempted to send an unauthenticated Query')
19
- return null
21
+ return new Observable((observer) => {
22
+ observer.error(new Error('No authentication token available'))
23
+ })
20
24
  }
21
25
 
22
26
  operation.setContext(({ headers = {} }) => ({
@@ -1,4 +1,4 @@
1
- import { BatchHttpLink as ApolloBatchHttpLink } from '@apollo/client/link/batch-http/index.js'
1
+ import { BatchHttpLink as ApolloBatchHttpLink } from '@apollo/client/link/batch-http'
2
2
  import crossfetch from 'cross-fetch'
3
3
 
4
4
  import { endpointGraphQL } from '@/configuration'
@@ -1,11 +1,15 @@
1
- import type { ServerError } from '@apollo/client/core'
2
- import { onError } from '@apollo/client/link/error/index.js'
1
+ import { ErrorLink as ApolloErrorLink } from '@apollo/client/link/error'
2
+ import type { GraphQLFormattedError } from 'graphql'
3
3
 
4
4
  import { GlobalStorage } from '@/storage'
5
5
 
6
- export const ErrorLink = onError(({ graphQLErrors, networkError }) => {
7
- if (graphQLErrors) {
8
- graphQLErrors.forEach(({ message, path, extensions }) => {
6
+ export const ErrorLink = new ApolloErrorLink(({ error, result }) => {
7
+ // In Apollo Client 4, errors are consolidated to the 'error' and 'result' properties
8
+
9
+ // Handle GraphQL errors from result
10
+ if (result?.errors) {
11
+ result.errors.forEach((graphQLError: GraphQLFormattedError) => {
12
+ const { message, path, extensions } = graphQLError
9
13
  const formattedPath = Array.isArray(path) ? path.join('.') : (path ?? 'N/A')
10
14
  const parts = [`[Quiltt][GraphQL Error]: ${message}`, `Path: ${formattedPath}`]
11
15
 
@@ -18,12 +22,15 @@ export const ErrorLink = onError(({ graphQLErrors, networkError }) => {
18
22
  })
19
23
  }
20
24
 
21
- if (networkError) {
22
- if ((networkError as ServerError).statusCode === 401) {
23
- console.warn('[Quiltt][Authentication Error]:', networkError)
25
+ // Handle network/server errors
26
+ if (error) {
27
+ if ('statusCode' in error && error.statusCode === 401) {
28
+ console.warn('[Quiltt][Authentication Error]:', error)
24
29
  GlobalStorage.set('session', null)
30
+ } else if ('statusCode' in error) {
31
+ console.warn('[Quiltt][Server Error]:', error)
25
32
  } else {
26
- console.warn('[Quiltt][Network Error]:', networkError)
33
+ console.warn('[Quiltt][Network Error]:', error)
27
34
  }
28
35
  }
29
36
  })
@@ -1,4 +1,4 @@
1
- import { ApolloLink } from '@apollo/client/core/index.js'
1
+ import { ApolloLink } from '@apollo/client/core'
2
2
 
3
3
  export const ForwardableLink = new ApolloLink((operation, forward) => forward(operation))
4
4
 
@@ -1,4 +1,4 @@
1
- import { HttpLink as ApolloHttpLink } from '@apollo/client/link/http/index.js'
1
+ import { HttpLink as ApolloHttpLink } from '@apollo/client/link/http'
2
2
  import crossfetch from 'cross-fetch'
3
3
 
4
4
  // Use `cross-fetch` only if `fetch` is not available on the `globalThis` object
@@ -1,8 +1,12 @@
1
- import { RetryLink as ApolloRetryLink } from '@apollo/client/link/retry/index.js'
1
+ import { RetryLink as ApolloRetryLink } from '@apollo/client/link/retry'
2
2
 
3
3
  export const RetryLink = new ApolloRetryLink({
4
4
  attempts: {
5
- retryIf: (error, _operation) => !!error && (!error.statusCode || error.statusCode >= 500),
5
+ retryIf: (error, _operation) => {
6
+ if (!error) return false
7
+ const statusCode = 'statusCode' in error ? (error as any).statusCode : undefined
8
+ return !statusCode || statusCode >= 500
9
+ },
6
10
  },
7
11
  })
8
12
 
@@ -1,5 +1,10 @@
1
- import { ApolloLink } from '@apollo/client/core/index.js'
1
+ import { ApolloLink } from '@apollo/client/core'
2
+ import { Observable } from 'rxjs'
2
3
 
3
- export const TerminatingLink = new ApolloLink(() => null)
4
+ export const TerminatingLink = new ApolloLink(() => {
5
+ return new Observable((observer) => {
6
+ observer.complete()
7
+ })
8
+ })
4
9
 
5
10
  export default TerminatingLink
@@ -1,15 +1,20 @@
1
- import { ApolloLink } from '@apollo/client/core/index.js'
1
+ import { ApolloLink } from '@apollo/client/core'
2
2
 
3
3
  import { version } from '@/configuration'
4
+ import { extractVersionNumber, getUserAgent } from '@/utils/telemetry'
4
5
 
5
- export const VersionLink = new ApolloLink((operation, forward) => {
6
- operation.setContext(({ headers = {} }) => ({
7
- headers: {
8
- ...headers,
9
- 'Quiltt-Client-Version': version,
10
- },
11
- }))
12
- return forward(operation)
13
- })
6
+ export const createVersionLink = (platformInfo: string) => {
7
+ const versionNumber = extractVersionNumber(version)
8
+ const userAgent = getUserAgent(versionNumber, platformInfo)
14
9
 
15
- export default VersionLink
10
+ return new ApolloLink((operation, forward) => {
11
+ operation.setContext(({ headers = {} }) => ({
12
+ headers: {
13
+ ...headers,
14
+ 'Quiltt-Client-Version': version,
15
+ 'User-Agent': userAgent,
16
+ },
17
+ }))
18
+ return forward(operation)
19
+ })
20
+ }
@@ -1,4 +1,5 @@
1
- import { endpointRest } from '@/configuration'
1
+ import { endpointRest, version } from '@/configuration'
2
+ import { extractVersionNumber, getUserAgent } from '@/utils/telemetry'
2
3
 
3
4
  import type { FetchResponse } from './fetchWithRetry'
4
5
  import { fetchWithRetry } from './fetchWithRetry'
@@ -17,11 +18,14 @@ export type ResolvableResponse = FetchResponse<ResolvableData>
17
18
 
18
19
  export class ConnectorsAPI {
19
20
  clientId: string
20
- agent: string
21
+ userAgent: string
21
22
 
22
- constructor(clientId: string, agent = 'web') {
23
+ constructor(
24
+ clientId: string,
25
+ userAgent: string = getUserAgent(extractVersionNumber(version), 'Unknown')
26
+ ) {
23
27
  this.clientId = clientId
24
- this.agent = agent
28
+ this.userAgent = userAgent
25
29
  }
26
30
 
27
31
  /**
@@ -87,7 +91,7 @@ export class ConnectorsAPI {
87
91
  const headers = new Headers()
88
92
  headers.set('Content-Type', 'application/json')
89
93
  headers.set('Accept', 'application/json')
90
- headers.set('Quiltt-SDK-Agent', this.agent)
94
+ headers.set('User-Agent', this.userAgent)
91
95
  headers.set('Authorization', `Bearer ${token}`)
92
96
 
93
97
  return {
package/src/index.ts CHANGED
@@ -5,3 +5,4 @@ export * from './Observable'
5
5
  export * from './storage'
6
6
  export * from './Timeoutable'
7
7
  export * from './types'
8
+ export * from './utils/telemetry'
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Extracts version number from formatted version string
3
+ * @param formattedVersion - Formatted version like "@quiltt/core: v4.5.1"
4
+ * @returns Version number like "4.5.1" or "unknown" if not found
5
+ */
6
+ export const extractVersionNumber = (formattedVersion: string): string => {
7
+ // Find the 'v' prefix and extract version after it
8
+ const vIndex = formattedVersion.indexOf('v')
9
+ if (vIndex === -1) return 'unknown'
10
+
11
+ const versionPart = formattedVersion.substring(vIndex + 1)
12
+ const parts = versionPart.split('.')
13
+
14
+ // Validate we have at least major.minor.patch
15
+ if (parts.length < 3) return 'unknown'
16
+
17
+ // Extract numeric parts (handles cases like "4.5.1-beta")
18
+ const major = parts[0].match(/^\d+/)?.[0]
19
+ const minor = parts[1].match(/^\d+/)?.[0]
20
+ const patch = parts[2].match(/^\d+/)?.[0]
21
+
22
+ if (!major || !minor || !patch) return 'unknown'
23
+
24
+ return `${major}.${minor}.${patch}`
25
+ }
26
+
27
+ /**
28
+ * Generates a User-Agent string following standard format
29
+ * Format: Quiltt/<version> (<platform-info>)
30
+ */
31
+ export const getUserAgent = (sdkVersion: string, platformInfo: string): string => {
32
+ return `Quiltt/${sdkVersion} (${platformInfo})`
33
+ }
34
+
35
+ /**
36
+ * Detects browser information from user agent string
37
+ * Returns browser name and version, or 'Unknown' if not detected
38
+ */
39
+ export const getBrowserInfo = (): string => {
40
+ if (typeof navigator === 'undefined' || !navigator.userAgent) {
41
+ return 'Unknown'
42
+ }
43
+
44
+ const ua = navigator.userAgent
45
+
46
+ // Edge (must be checked before Chrome)
47
+ if (ua.includes('Edg/')) {
48
+ const version = ua.match(/Edg\/(\d+)/)?.[1] || 'Unknown'
49
+ return `Edge/${version}`
50
+ }
51
+
52
+ // Chrome
53
+ if (ua.includes('Chrome/') && !ua.includes('Edg/')) {
54
+ const version = ua.match(/Chrome\/(\d+)/)?.[1] || 'Unknown'
55
+ return `Chrome/${version}`
56
+ }
57
+
58
+ // Safari (must be checked after Chrome)
59
+ if (ua.includes('Safari/') && !ua.includes('Chrome/')) {
60
+ const version = ua.match(/Version\/(\d+)/)?.[1] || 'Unknown'
61
+ return `Safari/${version}`
62
+ }
63
+
64
+ // Firefox
65
+ if (ua.includes('Firefox/')) {
66
+ const version = ua.match(/Firefox\/(\d+)/)?.[1] || 'Unknown'
67
+ return `Firefox/${version}`
68
+ }
69
+
70
+ return 'Unknown'
71
+ }